mirror of
https://gitlab.com/allianceauth/allianceauth.git
synced 2026-02-04 14:16:21 +01:00
Compare commits
66 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da93940e13 | ||
|
|
f53b43d9dc | ||
|
|
497a167ca7 | ||
|
|
852c5a3037 | ||
|
|
90f6777a7a | ||
|
|
a8d890abaf | ||
|
|
79379b444c | ||
|
|
ace1de5c68 | ||
|
|
5d6128e9ea | ||
|
|
131cc5ed0a | ||
|
|
9297bed43f | ||
|
|
b2fddc683a | ||
|
|
9af634d16a | ||
|
|
a68163caa3 | ||
|
|
00770fd034 | ||
|
|
01164777ed | ||
|
|
00f5e3e1e0 | ||
|
|
8b2527f408 | ||
|
|
b7500e4e4e | ||
|
|
4f4bd0c419 | ||
|
|
8ae4e02012 | ||
|
|
cc9a07197d | ||
|
|
f18dd1029b | ||
|
|
fd8d43571a | ||
|
|
13e88492f1 | ||
|
|
38df580a56 | ||
|
|
ba39318313 | ||
|
|
d8c6035405 | ||
|
|
2ef3da916b | ||
|
|
d32d8b26ce | ||
|
|
f348b1a34c | ||
|
|
86aaa3edda | ||
|
|
26017056c7 | ||
|
|
e39a3c072b | ||
|
|
827291dda4 | ||
|
|
ea8958ccc3 | ||
|
|
20554df857 | ||
|
|
750f43eaf0 | ||
|
|
09cf28ec9f | ||
|
|
b61746b3cb | ||
|
|
22c22fafeb | ||
|
|
577c4395c4 | ||
|
|
d241f476f7 | ||
|
|
5832ed0c30 | ||
|
|
bd9ea225be | ||
|
|
4a575dd70c | ||
|
|
b80ee16a7c | ||
|
|
c888371e6c | ||
|
|
8de2c3bfcb | ||
|
|
6688f73565 | ||
|
|
7d929cb6e2 | ||
|
|
72740b9e4d | ||
|
|
f7d279fa16 | ||
|
|
ff7c9c48f3 | ||
|
|
d11832913d | ||
|
|
724e0e83f2 | ||
|
|
333f091f1a | ||
|
|
cfbb0b993a | ||
|
|
582b6754a4 | ||
|
|
dfe62db8ee | ||
|
|
52ae05d057 | ||
|
|
f17ebbede6 | ||
|
|
a19302babc | ||
|
|
18a627b01e | ||
|
|
eddb5480e9 | ||
|
|
5b26757662 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -76,3 +76,4 @@ celerybeat-schedule
|
||||
.flake8
|
||||
.pylintrc
|
||||
Makefile
|
||||
.isort.cfg
|
||||
|
||||
@@ -86,6 +86,17 @@ test-3.10-core:
|
||||
reports:
|
||||
cobertura: coverage.xml
|
||||
|
||||
test-3.11-core:
|
||||
<<: *only-default
|
||||
image: python:3.11-rc-bullseye
|
||||
script:
|
||||
- tox -e py311-core
|
||||
artifacts:
|
||||
when: always
|
||||
reports:
|
||||
cobertura: coverage.xml
|
||||
allow_failure: true
|
||||
|
||||
test-3.7-all:
|
||||
<<: *only-default
|
||||
image: python:3.7-bullseye
|
||||
@@ -126,6 +137,17 @@ test-3.10-all:
|
||||
reports:
|
||||
cobertura: coverage.xml
|
||||
|
||||
test-3.11-all:
|
||||
<<: *only-default
|
||||
image: python:3.11-rc-bullseye
|
||||
script:
|
||||
- tox -e py311-all
|
||||
artifacts:
|
||||
when: always
|
||||
reports:
|
||||
cobertura: coverage.xml
|
||||
allow_failure: true
|
||||
|
||||
deploy_production:
|
||||
stage: deploy
|
||||
image: python:3.10-bullseye
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# This will make sure the app is always imported when
|
||||
# Django starts so that shared_task will use this app.
|
||||
|
||||
__version__ = '2.9.3'
|
||||
__version__ = '2.11.2'
|
||||
__title__ = 'Alliance Auth'
|
||||
__url__ = 'https://gitlab.com/allianceauth/allianceauth'
|
||||
NAME = f'{__title__} v{__version__}'
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils.deprecation import MiddlewareMixin
|
||||
from .models import AnalyticsTokens, AnalyticsIdentifier
|
||||
from .tasks import send_ga_tracking_web_view
|
||||
@@ -10,6 +11,8 @@ import re
|
||||
class AnalyticsMiddleware(MiddlewareMixin):
|
||||
def process_response(self, request, response):
|
||||
"""Django Middleware: Process Page Views and creates Analytics Celery Tasks"""
|
||||
if getattr(settings, "ANALYTICS_DISABLED", False):
|
||||
return response
|
||||
analyticstokens = AnalyticsTokens.objects.all()
|
||||
client_id = AnalyticsIdentifier.objects.get(id=1).identifier.hex
|
||||
try:
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# Generated by Django 3.1.13 on 2021-10-15 05:02
|
||||
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def modify_aa_team_token_add_page_ignore_paths(apps, schema_editor):
|
||||
# We can't import the Person model directly as it may be a newer
|
||||
# version than this migration expects. We use the historical version.
|
||||
# Add /admin/ and /user_notifications_count/ path to ignore
|
||||
|
||||
AnalyticsPath = apps.get_model('analytics', 'AnalyticsPath')
|
||||
admin = AnalyticsPath.objects.create(ignore_path=r"^\/admin\/.*")
|
||||
@@ -17,8 +17,19 @@ def modify_aa_team_token_add_page_ignore_paths(apps, schema_editor):
|
||||
|
||||
|
||||
def undo_modify_aa_team_token_add_page_ignore_paths(apps, schema_editor):
|
||||
# nothing should need to migrate away here?
|
||||
return True
|
||||
#
|
||||
AnalyticsPath = apps.get_model('analytics', 'AnalyticsPath')
|
||||
Tokens = apps.get_model('analytics', 'AnalyticsTokens')
|
||||
|
||||
token = Tokens.objects.get(token="UA-186249766-2")
|
||||
try:
|
||||
admin = AnalyticsPath.objects.get(ignore_path=r"^\/admin\/.*", analyticstokens=token)
|
||||
user_notifications_count = AnalyticsPath.objects.get(ignore_path=r"^\/user_notifications_count\/.*", analyticstokens=token)
|
||||
admin.delete()
|
||||
user_notifications_count.delete()
|
||||
except ObjectDoesNotExist:
|
||||
# Its fine if it doesnt exist, we just dont want them building up when re-migrating
|
||||
pass
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
40
allianceauth/analytics/migrations/0006_more_ignore_paths.py
Normal file
40
allianceauth/analytics/migrations/0006_more_ignore_paths.py
Normal file
@@ -0,0 +1,40 @@
|
||||
# Generated by Django 3.2.8 on 2021-10-19 01:47
|
||||
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def modify_aa_team_token_add_page_ignore_paths(apps, schema_editor):
|
||||
# Add the /account/activate path to ignore
|
||||
|
||||
AnalyticsPath = apps.get_model('analytics', 'AnalyticsPath')
|
||||
account_activate = AnalyticsPath.objects.create(ignore_path=r"^\/account\/activate\/.*")
|
||||
|
||||
Tokens = apps.get_model('analytics', 'AnalyticsTokens')
|
||||
token = Tokens.objects.get(token="UA-186249766-2")
|
||||
token.ignore_paths.add(account_activate)
|
||||
|
||||
|
||||
def undo_modify_aa_team_token_add_page_ignore_paths(apps, schema_editor):
|
||||
#
|
||||
AnalyticsPath = apps.get_model('analytics', 'AnalyticsPath')
|
||||
Tokens = apps.get_model('analytics', 'AnalyticsTokens')
|
||||
|
||||
token = Tokens.objects.get(token="UA-186249766-2")
|
||||
|
||||
try:
|
||||
account_activate = AnalyticsPath.objects.get(ignore_path=r"^\/account\/activate\/.*", analyticstokens=token)
|
||||
account_activate.delete()
|
||||
except ObjectDoesNotExist:
|
||||
# Its fine if it doesnt exist, we just dont want them building up when re-migrating
|
||||
pass
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('analytics', '0005_alter_analyticspath_ignore_path'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(modify_aa_team_token_add_page_ignore_paths, undo_modify_aa_team_token_add_page_ignore_paths)
|
||||
]
|
||||
@@ -1,7 +1,8 @@
|
||||
from allianceauth.analytics.tasks import analytics_event
|
||||
from celery.signals import task_failure, task_success
|
||||
|
||||
import logging
|
||||
from celery.signals import task_failure, task_success
|
||||
from django.conf import settings
|
||||
from allianceauth.analytics.tasks import analytics_event
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -11,6 +12,8 @@ def process_failure_signal(
|
||||
sender, task_id, signal,
|
||||
args, kwargs, einfo, **kw):
|
||||
logger.debug("Celery task_failure signal %s" % sender.__class__.__name__)
|
||||
if getattr(settings, "ANALYTICS_DISABLED", False):
|
||||
return
|
||||
|
||||
category = sender.__module__
|
||||
|
||||
@@ -30,6 +33,8 @@ def process_failure_signal(
|
||||
@task_success.connect
|
||||
def celery_success_signal(sender, result=None, **kw):
|
||||
logger.debug("Celery task_success signal %s" % sender.__class__.__name__)
|
||||
if getattr(settings, "ANALYTICS_DISABLED", False):
|
||||
return
|
||||
|
||||
category = sender.__module__
|
||||
|
||||
|
||||
@@ -21,8 +21,8 @@ if getattr(settings, "ANALYTICS_ENABLE_DEBUG", False) and settings.DEBUG:
|
||||
# Force sending of analytics data during in a debug/test environemt
|
||||
# Usefull for developers working on this feature.
|
||||
logger.warning(
|
||||
"You have 'ANALYTICS_ENABLE_DEBUG' Enabled! "
|
||||
"This debug instance will send analytics data!")
|
||||
"You have 'ANALYTICS_ENABLE_DEBUG' Enabled! "
|
||||
"This debug instance will send analytics data!")
|
||||
DEBUG_URL = COLLECTION_URL
|
||||
|
||||
ANALYTICS_URL = COLLECTION_URL
|
||||
@@ -40,13 +40,12 @@ def analytics_event(category: str,
|
||||
Send a Google Analytics Event for each token stored
|
||||
Includes check for if its enabled/disabled
|
||||
|
||||
Parameters
|
||||
-------
|
||||
`category` (str): Celery Namespace
|
||||
`action` (str): Task Name
|
||||
`label` (str): Optional, Task Success/Exception
|
||||
`value` (int): Optional, If bulk, Query size, can be a binary True/False
|
||||
`event_type` (str): Optional, Celery or Stats only, Default to Celery
|
||||
Args:
|
||||
`category` (str): Celery Namespace
|
||||
`action` (str): Task Name
|
||||
`label` (str): Optional, Task Success/Exception
|
||||
`value` (int): Optional, If bulk, Query size, can be a binary True/False
|
||||
`event_type` (str): Optional, Celery or Stats only, Default to Celery
|
||||
"""
|
||||
analyticstokens = AnalyticsTokens.objects.all()
|
||||
client_id = AnalyticsIdentifier.objects.get(id=1).identifier.hex
|
||||
@@ -60,20 +59,21 @@ def analytics_event(category: str,
|
||||
|
||||
if allowed is True:
|
||||
tracking_id = token.token
|
||||
send_ga_tracking_celery_event.s(tracking_id=tracking_id,
|
||||
client_id=client_id,
|
||||
category=category,
|
||||
action=action,
|
||||
label=label,
|
||||
value=value).\
|
||||
apply_async(priority=9)
|
||||
send_ga_tracking_celery_event.s(
|
||||
tracking_id=tracking_id,
|
||||
client_id=client_id,
|
||||
category=category,
|
||||
action=action,
|
||||
label=label,
|
||||
value=value).apply_async(priority=9)
|
||||
|
||||
|
||||
@shared_task()
|
||||
def analytics_daily_stats():
|
||||
"""Celery Task: Do not call directly
|
||||
|
||||
Gathers a series of daily statistics and sends analytics events containing them"""
|
||||
Gathers a series of daily statistics and sends analytics events containing them
|
||||
"""
|
||||
users = install_stat_users()
|
||||
tokens = install_stat_tokens()
|
||||
addons = install_stat_addons()
|
||||
|
||||
108
allianceauth/analytics/tests/test_integration.py
Normal file
108
allianceauth/analytics/tests/test_integration.py
Normal file
@@ -0,0 +1,108 @@
|
||||
from unittest.mock import patch
|
||||
from urllib.parse import parse_qs
|
||||
|
||||
import requests_mock
|
||||
|
||||
from django.test import TestCase, override_settings
|
||||
|
||||
from allianceauth.analytics.tasks import ANALYTICS_URL
|
||||
from allianceauth.eveonline.tasks import update_character
|
||||
from allianceauth.tests.auth_utils import AuthUtils
|
||||
|
||||
|
||||
@override_settings(CELERY_ALWAYS_EAGER=True)
|
||||
@requests_mock.mock()
|
||||
class TestAnalyticsForViews(TestCase):
|
||||
@override_settings(ANALYTICS_DISABLED=False)
|
||||
def test_should_run_analytics(self, requests_mocker):
|
||||
# given
|
||||
requests_mocker.post(ANALYTICS_URL)
|
||||
user = AuthUtils.create_user("Bruce Wayne")
|
||||
self.client.force_login(user)
|
||||
# when
|
||||
response = self.client.get("/dashboard/")
|
||||
# then
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTrue(requests_mocker.called)
|
||||
|
||||
@override_settings(ANALYTICS_DISABLED=True)
|
||||
def test_should_not_run_analytics(self, requests_mocker):
|
||||
# given
|
||||
requests_mocker.post(ANALYTICS_URL)
|
||||
user = AuthUtils.create_user("Bruce Wayne")
|
||||
self.client.force_login(user)
|
||||
# when
|
||||
response = self.client.get("/dashboard/")
|
||||
# then
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertFalse(requests_mocker.called)
|
||||
|
||||
|
||||
@override_settings(CELERY_ALWAYS_EAGER=True)
|
||||
@requests_mock.mock()
|
||||
class TestAnalyticsForTasks(TestCase):
|
||||
@override_settings(ANALYTICS_DISABLED=False)
|
||||
@patch("allianceauth.eveonline.models.EveCharacter.objects.update_character")
|
||||
def test_should_run_analytics_for_successful_task(
|
||||
self, requests_mocker, mock_update_character
|
||||
):
|
||||
# given
|
||||
requests_mocker.post(ANALYTICS_URL)
|
||||
user = AuthUtils.create_user("Bruce Wayne")
|
||||
character = AuthUtils.add_main_character_2(user, "Bruce Wayne", 1001)
|
||||
# when
|
||||
update_character.delay(character.character_id)
|
||||
# then
|
||||
self.assertTrue(mock_update_character.called)
|
||||
self.assertTrue(requests_mocker.called)
|
||||
payload = parse_qs(requests_mocker.last_request.text)
|
||||
self.assertListEqual(payload["el"], ["Success"])
|
||||
|
||||
@override_settings(ANALYTICS_DISABLED=True)
|
||||
@patch("allianceauth.eveonline.models.EveCharacter.objects.update_character")
|
||||
def test_should_not_run_analytics_for_successful_task(
|
||||
self, requests_mocker, mock_update_character
|
||||
):
|
||||
# given
|
||||
requests_mocker.post(ANALYTICS_URL)
|
||||
user = AuthUtils.create_user("Bruce Wayne")
|
||||
character = AuthUtils.add_main_character_2(user, "Bruce Wayne", 1001)
|
||||
# when
|
||||
update_character.delay(character.character_id)
|
||||
# then
|
||||
self.assertTrue(mock_update_character.called)
|
||||
self.assertFalse(requests_mocker.called)
|
||||
|
||||
@override_settings(ANALYTICS_DISABLED=False)
|
||||
@patch("allianceauth.eveonline.models.EveCharacter.objects.update_character")
|
||||
def test_should_run_analytics_for_failed_task(
|
||||
self, requests_mocker, mock_update_character
|
||||
):
|
||||
# given
|
||||
requests_mocker.post(ANALYTICS_URL)
|
||||
mock_update_character.side_effect = RuntimeError
|
||||
user = AuthUtils.create_user("Bruce Wayne")
|
||||
character = AuthUtils.add_main_character_2(user, "Bruce Wayne", 1001)
|
||||
# when
|
||||
update_character.delay(character.character_id)
|
||||
# then
|
||||
self.assertTrue(mock_update_character.called)
|
||||
self.assertTrue(requests_mocker.called)
|
||||
payload = parse_qs(requests_mocker.last_request.text)
|
||||
self.assertNotEqual(payload["el"], ["Success"])
|
||||
|
||||
@override_settings(ANALYTICS_DISABLED=True)
|
||||
@patch("allianceauth.eveonline.models.EveCharacter.objects.update_character")
|
||||
def test_should_not_run_analytics_for_failed_task(
|
||||
self, requests_mocker, mock_update_character
|
||||
):
|
||||
# given
|
||||
requests_mocker.post(ANALYTICS_URL)
|
||||
mock_update_character.side_effect = RuntimeError
|
||||
user = AuthUtils.create_user("Bruce Wayne")
|
||||
character = AuthUtils.add_main_character_2(user, "Bruce Wayne", 1001)
|
||||
# when
|
||||
update_character.delay(character.character_id)
|
||||
# then
|
||||
self.assertTrue(mock_update_character.called)
|
||||
self.assertFalse(requests_mocker.called)
|
||||
@@ -380,6 +380,7 @@ class UserAdmin(BaseUserAdmin):
|
||||
'username',
|
||||
'character_ownerships__character__character_name'
|
||||
)
|
||||
readonly_fields = ('date_joined', 'last_login')
|
||||
|
||||
def _characters(self, obj):
|
||||
character_ownerships = list(obj.character_ownerships.all())
|
||||
@@ -425,10 +426,19 @@ class UserAdmin(BaseUserAdmin):
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
return request.user.has_perm('auth.delete_user')
|
||||
|
||||
def get_object(self, *args , **kwargs):
|
||||
obj = super().get_object(*args , **kwargs)
|
||||
self.obj = obj # storing current object for use in formfield_for_manytomany
|
||||
return obj
|
||||
|
||||
def formfield_for_manytomany(self, db_field, request, **kwargs):
|
||||
"""overriding this formfield to have sorted lists in the form"""
|
||||
if db_field.name == "groups":
|
||||
kwargs["queryset"] = Group.objects.all().order_by(Lower('name'))
|
||||
groups_qs = Group.objects.filter(authgroup__states__isnull=True)
|
||||
obj_state = self.obj.profile.state
|
||||
if obj_state:
|
||||
matching_groups_qs = Group.objects.filter(authgroup__states=obj_state)
|
||||
groups_qs = groups_qs | matching_groups_qs
|
||||
kwargs["queryset"] = groups_qs.order_by(Lower('name'))
|
||||
return super().formfield_for_manytomany(db_field, request, **kwargs)
|
||||
|
||||
|
||||
|
||||
@@ -3,10 +3,14 @@ from django.core.checks import register, Tags
|
||||
|
||||
|
||||
class AuthenticationConfig(AppConfig):
|
||||
name = 'allianceauth.authentication'
|
||||
label = 'authentication'
|
||||
name = "allianceauth.authentication"
|
||||
label = "authentication"
|
||||
|
||||
def ready(self):
|
||||
super().ready()
|
||||
from allianceauth.authentication import checks, signals
|
||||
from allianceauth.authentication import checks, signals # noqa: F401
|
||||
from allianceauth.authentication.task_statistics import (
|
||||
signals as celery_signals,
|
||||
)
|
||||
|
||||
register(Tags.security)(checks.check_login_scopes_setting)
|
||||
celery_signals.reset_counters()
|
||||
|
||||
40
allianceauth/authentication/task_statistics/counters.py
Normal file
40
allianceauth/authentication/task_statistics/counters.py
Normal file
@@ -0,0 +1,40 @@
|
||||
from collections import namedtuple
|
||||
import datetime as dt
|
||||
|
||||
from .event_series import EventSeries
|
||||
|
||||
|
||||
"""Global series for counting task events."""
|
||||
succeeded_tasks = EventSeries("SUCCEEDED_TASKS")
|
||||
retried_tasks = EventSeries("RETRIED_TASKS")
|
||||
failed_tasks = EventSeries("FAILED_TASKS")
|
||||
|
||||
|
||||
_TaskCounts = namedtuple(
|
||||
"_TaskCounts", ["succeeded", "retried", "failed", "total", "earliest_task", "hours"]
|
||||
)
|
||||
|
||||
|
||||
def dashboard_results(hours: int) -> _TaskCounts:
|
||||
"""Counts of all task events within the given timeframe."""
|
||||
|
||||
def earliest_if_exists(events: EventSeries, earliest: dt.datetime) -> list:
|
||||
my_earliest = events.first_event(earliest=earliest)
|
||||
return [my_earliest] if my_earliest else []
|
||||
|
||||
earliest = dt.datetime.utcnow() - dt.timedelta(hours=hours)
|
||||
earliest_events = list()
|
||||
succeeded_count = succeeded_tasks.count(earliest=earliest)
|
||||
earliest_events += earliest_if_exists(succeeded_tasks, earliest)
|
||||
retried_count = retried_tasks.count(earliest=earliest)
|
||||
earliest_events += earliest_if_exists(retried_tasks, earliest)
|
||||
failed_count = failed_tasks.count(earliest=earliest)
|
||||
earliest_events += earliest_if_exists(failed_tasks, earliest)
|
||||
return _TaskCounts(
|
||||
succeeded=succeeded_count,
|
||||
retried=retried_count,
|
||||
failed=failed_count,
|
||||
total=succeeded_count + retried_count + failed_count,
|
||||
earliest_task=min(earliest_events) if earliest_events else None,
|
||||
hours=hours,
|
||||
)
|
||||
95
allianceauth/authentication/task_statistics/event_series.py
Normal file
95
allianceauth/authentication/task_statistics/event_series.py
Normal file
@@ -0,0 +1,95 @@
|
||||
import datetime as dt
|
||||
from typing import Optional, List
|
||||
|
||||
from redis import Redis
|
||||
from pytz import utc
|
||||
|
||||
from django.core.cache import cache
|
||||
|
||||
|
||||
class EventSeries:
|
||||
"""API for recording and analysing a series of events."""
|
||||
|
||||
_ROOT_KEY = "ALLIANCEAUTH_EVENT_SERIES"
|
||||
|
||||
def __init__(self, key_id: str, redis: Redis = None) -> None:
|
||||
self._redis = cache.get_master_client() if not redis else redis
|
||||
if not isinstance(self._redis, Redis):
|
||||
raise TypeError(
|
||||
"This class requires a Redis client, but none was provided "
|
||||
"and the default Django cache backend is not Redis either."
|
||||
)
|
||||
self._key_id = str(key_id)
|
||||
self.clear()
|
||||
|
||||
@property
|
||||
def _key_counter(self):
|
||||
return f"{self._ROOT_KEY}_{self._key_id}_COUNTER"
|
||||
|
||||
@property
|
||||
def _key_sorted_set(self):
|
||||
return f"{self._ROOT_KEY}_{self._key_id}_SORTED_SET"
|
||||
|
||||
def add(self, event_time: dt.datetime = None) -> None:
|
||||
"""Add event.
|
||||
|
||||
Args:
|
||||
- event_time: timestamp of event. Will use current time if not specified.
|
||||
"""
|
||||
if not event_time:
|
||||
event_time = dt.datetime.utcnow()
|
||||
id = self._redis.incr(self._key_counter)
|
||||
self._redis.zadd(self._key_sorted_set, {id: event_time.timestamp()})
|
||||
|
||||
def all(self) -> List[dt.datetime]:
|
||||
"""List of all known events."""
|
||||
return [
|
||||
event[1]
|
||||
for event in self._redis.zrangebyscore(
|
||||
self._key_sorted_set,
|
||||
"-inf",
|
||||
"+inf",
|
||||
withscores=True,
|
||||
score_cast_func=self._cast_scores_to_dt,
|
||||
)
|
||||
]
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Clear all events."""
|
||||
self._redis.delete(self._key_sorted_set)
|
||||
self._redis.delete(self._key_counter)
|
||||
|
||||
def count(self, earliest: dt.datetime = None, latest: dt.datetime = None) -> int:
|
||||
"""Count of events, can be restricted to given timeframe.
|
||||
|
||||
Args:
|
||||
- earliest: Date of first events to count(inclusive), or -infinite if not specified
|
||||
- latest: Date of last events to count(inclusive), or +infinite if not specified
|
||||
"""
|
||||
min = "-inf" if not earliest else earliest.timestamp()
|
||||
max = "+inf" if not latest else latest.timestamp()
|
||||
return self._redis.zcount(self._key_sorted_set, min=min, max=max)
|
||||
|
||||
def first_event(self, earliest: dt.datetime = None) -> Optional[dt.datetime]:
|
||||
"""Date/Time of first event. Returns `None` if series has no events.
|
||||
|
||||
Args:
|
||||
- earliest: Date of first events to count(inclusive), or any if not specified
|
||||
"""
|
||||
min = "-inf" if not earliest else earliest.timestamp()
|
||||
event = self._redis.zrangebyscore(
|
||||
self._key_sorted_set,
|
||||
min,
|
||||
"+inf",
|
||||
withscores=True,
|
||||
start=0,
|
||||
num=1,
|
||||
score_cast_func=self._cast_scores_to_dt,
|
||||
)
|
||||
if not event:
|
||||
return None
|
||||
return event[0][1]
|
||||
|
||||
@staticmethod
|
||||
def _cast_scores_to_dt(score) -> dt.datetime:
|
||||
return dt.datetime.fromtimestamp(float(score), tz=utc)
|
||||
54
allianceauth/authentication/task_statistics/signals.py
Normal file
54
allianceauth/authentication/task_statistics/signals.py
Normal file
@@ -0,0 +1,54 @@
|
||||
from celery.signals import (
|
||||
task_failure,
|
||||
task_internal_error,
|
||||
task_retry,
|
||||
task_success,
|
||||
worker_ready
|
||||
)
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from .counters import failed_tasks, retried_tasks, succeeded_tasks
|
||||
|
||||
|
||||
def reset_counters():
|
||||
"""Reset all counters for the celery status."""
|
||||
succeeded_tasks.clear()
|
||||
failed_tasks.clear()
|
||||
retried_tasks.clear()
|
||||
|
||||
|
||||
def is_enabled() -> bool:
|
||||
return not bool(
|
||||
getattr(settings, "ALLIANCEAUTH_DASHBOARD_TASK_STATISTICS_DISABLED", False)
|
||||
)
|
||||
|
||||
|
||||
@worker_ready.connect
|
||||
def reset_counters_when_celery_restarted(*args, **kwargs):
|
||||
if is_enabled():
|
||||
reset_counters()
|
||||
|
||||
|
||||
@task_success.connect
|
||||
def record_task_succeeded(*args, **kwargs):
|
||||
if is_enabled():
|
||||
succeeded_tasks.add()
|
||||
|
||||
|
||||
@task_retry.connect
|
||||
def record_task_retried(*args, **kwargs):
|
||||
if is_enabled():
|
||||
retried_tasks.add()
|
||||
|
||||
|
||||
@task_failure.connect
|
||||
def record_task_failed(*args, **kwargs):
|
||||
if is_enabled():
|
||||
failed_tasks.add()
|
||||
|
||||
|
||||
@task_internal_error.connect
|
||||
def record_task_internal_error(*args, **kwargs):
|
||||
if is_enabled():
|
||||
failed_tasks.add()
|
||||
@@ -0,0 +1,51 @@
|
||||
import datetime as dt
|
||||
|
||||
from django.test import TestCase
|
||||
from django.utils.timezone import now
|
||||
|
||||
from allianceauth.authentication.task_statistics.counters import (
|
||||
dashboard_results,
|
||||
succeeded_tasks,
|
||||
retried_tasks,
|
||||
failed_tasks,
|
||||
)
|
||||
|
||||
|
||||
class TestDashboardResults(TestCase):
|
||||
def test_should_return_counts_for_given_timeframe_only(self):
|
||||
# given
|
||||
earliest_task = now() - dt.timedelta(minutes=15)
|
||||
succeeded_tasks.clear()
|
||||
succeeded_tasks.add(now() - dt.timedelta(hours=1, seconds=1))
|
||||
succeeded_tasks.add(earliest_task)
|
||||
succeeded_tasks.add()
|
||||
succeeded_tasks.add()
|
||||
retried_tasks.clear()
|
||||
retried_tasks.add(now() - dt.timedelta(hours=1, seconds=1))
|
||||
retried_tasks.add(now() - dt.timedelta(seconds=30))
|
||||
retried_tasks.add()
|
||||
failed_tasks.clear()
|
||||
failed_tasks.add(now() - dt.timedelta(hours=1, seconds=1))
|
||||
failed_tasks.add()
|
||||
# when
|
||||
results = dashboard_results(hours=1)
|
||||
# then
|
||||
self.assertEqual(results.succeeded, 3)
|
||||
self.assertEqual(results.retried, 2)
|
||||
self.assertEqual(results.failed, 1)
|
||||
self.assertEqual(results.total, 6)
|
||||
self.assertEqual(results.earliest_task, earliest_task)
|
||||
|
||||
def test_should_work_with_no_data(self):
|
||||
# given
|
||||
succeeded_tasks.clear()
|
||||
retried_tasks.clear()
|
||||
failed_tasks.clear()
|
||||
# when
|
||||
results = dashboard_results(hours=1)
|
||||
# then
|
||||
self.assertEqual(results.succeeded, 0)
|
||||
self.assertEqual(results.retried, 0)
|
||||
self.assertEqual(results.failed, 0)
|
||||
self.assertEqual(results.total, 0)
|
||||
self.assertIsNone(results.earliest_task)
|
||||
@@ -0,0 +1,133 @@
|
||||
import datetime as dt
|
||||
|
||||
from pytz import utc
|
||||
from django.test import TestCase
|
||||
from django.utils.timezone import now
|
||||
|
||||
from allianceauth.authentication.task_statistics.event_series import EventSeries
|
||||
|
||||
|
||||
class TestEventSeries(TestCase):
|
||||
def test_should_add_event(self):
|
||||
# given
|
||||
events = EventSeries("dummy")
|
||||
# when
|
||||
events.add()
|
||||
# then
|
||||
result = events.all()
|
||||
self.assertEqual(len(result), 1)
|
||||
self.assertAlmostEqual(result[0], now(), delta=dt.timedelta(seconds=30))
|
||||
|
||||
def test_should_add_event_with_specified_time(self):
|
||||
# given
|
||||
events = EventSeries("dummy")
|
||||
my_time = dt.datetime(2021, 11, 1, 12, 15, tzinfo=utc)
|
||||
# when
|
||||
events.add(my_time)
|
||||
# then
|
||||
result = events.all()
|
||||
self.assertEqual(len(result), 1)
|
||||
self.assertAlmostEqual(result[0], my_time, delta=dt.timedelta(seconds=30))
|
||||
|
||||
def test_should_count_events(self):
|
||||
# given
|
||||
events = EventSeries("dummy")
|
||||
events.add()
|
||||
events.add()
|
||||
# when
|
||||
result = events.count()
|
||||
# then
|
||||
self.assertEqual(result, 2)
|
||||
|
||||
def test_should_count_zero(self):
|
||||
# given
|
||||
events = EventSeries("dummy")
|
||||
# when
|
||||
result = events.count()
|
||||
# then
|
||||
self.assertEqual(result, 0)
|
||||
|
||||
def test_should_count_events_within_timeframe_1(self):
|
||||
# given
|
||||
events = EventSeries("dummy")
|
||||
events.add(dt.datetime(2021, 12, 1, 12, 0, tzinfo=utc))
|
||||
events.add(dt.datetime(2021, 12, 1, 12, 10, tzinfo=utc))
|
||||
events.add(dt.datetime(2021, 12, 1, 12, 15, tzinfo=utc))
|
||||
events.add(dt.datetime(2021, 12, 1, 12, 30, tzinfo=utc))
|
||||
# when
|
||||
result = events.count(
|
||||
earliest=dt.datetime(2021, 12, 1, 12, 8, tzinfo=utc),
|
||||
latest=dt.datetime(2021, 12, 1, 12, 17, tzinfo=utc),
|
||||
)
|
||||
# then
|
||||
self.assertEqual(result, 2)
|
||||
|
||||
def test_should_count_events_within_timeframe_2(self):
|
||||
# given
|
||||
events = EventSeries("dummy")
|
||||
events.add(dt.datetime(2021, 12, 1, 12, 0, tzinfo=utc))
|
||||
events.add(dt.datetime(2021, 12, 1, 12, 10, tzinfo=utc))
|
||||
events.add(dt.datetime(2021, 12, 1, 12, 15, tzinfo=utc))
|
||||
events.add(dt.datetime(2021, 12, 1, 12, 30, tzinfo=utc))
|
||||
# when
|
||||
result = events.count(earliest=dt.datetime(2021, 12, 1, 12, 8))
|
||||
# then
|
||||
self.assertEqual(result, 3)
|
||||
|
||||
def test_should_count_events_within_timeframe_3(self):
|
||||
# given
|
||||
events = EventSeries("dummy")
|
||||
events.add(dt.datetime(2021, 12, 1, 12, 0, tzinfo=utc))
|
||||
events.add(dt.datetime(2021, 12, 1, 12, 10, tzinfo=utc))
|
||||
events.add(dt.datetime(2021, 12, 1, 12, 15, tzinfo=utc))
|
||||
events.add(dt.datetime(2021, 12, 1, 12, 30, tzinfo=utc))
|
||||
# when
|
||||
result = events.count(latest=dt.datetime(2021, 12, 1, 12, 12))
|
||||
# then
|
||||
self.assertEqual(result, 2)
|
||||
|
||||
def test_should_clear_events(self):
|
||||
# given
|
||||
events = EventSeries("dummy")
|
||||
events.add()
|
||||
events.add()
|
||||
# when
|
||||
events.clear()
|
||||
# then
|
||||
self.assertEqual(events.count(), 0)
|
||||
|
||||
def test_should_return_date_of_first_event(self):
|
||||
# given
|
||||
events = EventSeries("dummy")
|
||||
events.add(dt.datetime(2021, 12, 1, 12, 0, tzinfo=utc))
|
||||
events.add(dt.datetime(2021, 12, 1, 12, 10, tzinfo=utc))
|
||||
events.add(dt.datetime(2021, 12, 1, 12, 15, tzinfo=utc))
|
||||
events.add(dt.datetime(2021, 12, 1, 12, 30, tzinfo=utc))
|
||||
# when
|
||||
result = events.first_event()
|
||||
# then
|
||||
self.assertEqual(result, dt.datetime(2021, 12, 1, 12, 0, tzinfo=utc))
|
||||
|
||||
def test_should_return_date_of_first_event_with_range(self):
|
||||
# given
|
||||
events = EventSeries("dummy")
|
||||
events.add(dt.datetime(2021, 12, 1, 12, 0, tzinfo=utc))
|
||||
events.add(dt.datetime(2021, 12, 1, 12, 10, tzinfo=utc))
|
||||
events.add(dt.datetime(2021, 12, 1, 12, 15, tzinfo=utc))
|
||||
events.add(dt.datetime(2021, 12, 1, 12, 30, tzinfo=utc))
|
||||
# when
|
||||
result = events.first_event(
|
||||
earliest=dt.datetime(2021, 12, 1, 12, 8, tzinfo=utc)
|
||||
)
|
||||
# then
|
||||
self.assertEqual(result, dt.datetime(2021, 12, 1, 12, 10, tzinfo=utc))
|
||||
|
||||
def test_should_return_all_events(self):
|
||||
# given
|
||||
events = EventSeries("dummy")
|
||||
events.add()
|
||||
events.add()
|
||||
# when
|
||||
results = events.all()
|
||||
# then
|
||||
self.assertEqual(len(results), 2)
|
||||
@@ -0,0 +1,93 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
from celery.exceptions import Retry
|
||||
|
||||
from django.test import TestCase, override_settings
|
||||
|
||||
from allianceauth.authentication.task_statistics.counters import (
|
||||
failed_tasks,
|
||||
retried_tasks,
|
||||
succeeded_tasks,
|
||||
)
|
||||
from allianceauth.authentication.task_statistics.signals import (
|
||||
reset_counters,
|
||||
is_enabled,
|
||||
)
|
||||
from allianceauth.eveonline.tasks import update_character
|
||||
|
||||
|
||||
@override_settings(
|
||||
CELERY_ALWAYS_EAGER=True,ALLIANCEAUTH_DASHBOARD_TASK_STATISTICS_DISABLED=False
|
||||
)
|
||||
class TestTaskSignals(TestCase):
|
||||
fixtures = ["disable_analytics"]
|
||||
|
||||
def test_should_record_successful_task(self):
|
||||
# given
|
||||
succeeded_tasks.clear()
|
||||
retried_tasks.clear()
|
||||
failed_tasks.clear()
|
||||
# when
|
||||
with patch(
|
||||
"allianceauth.eveonline.tasks.EveCharacter.objects.update_character"
|
||||
) as mock_update:
|
||||
mock_update.return_value = None
|
||||
update_character.delay(1)
|
||||
# then
|
||||
self.assertEqual(succeeded_tasks.count(), 1)
|
||||
self.assertEqual(retried_tasks.count(), 0)
|
||||
self.assertEqual(failed_tasks.count(), 0)
|
||||
|
||||
def test_should_record_retried_task(self):
|
||||
# given
|
||||
succeeded_tasks.clear()
|
||||
retried_tasks.clear()
|
||||
failed_tasks.clear()
|
||||
# when
|
||||
with patch(
|
||||
"allianceauth.eveonline.tasks.EveCharacter.objects.update_character"
|
||||
) as mock_update:
|
||||
mock_update.side_effect = Retry
|
||||
update_character.delay(1)
|
||||
# then
|
||||
self.assertEqual(succeeded_tasks.count(), 0)
|
||||
self.assertEqual(failed_tasks.count(), 0)
|
||||
self.assertEqual(retried_tasks.count(), 1)
|
||||
|
||||
def test_should_record_failed_task(self):
|
||||
# given
|
||||
succeeded_tasks.clear()
|
||||
retried_tasks.clear()
|
||||
failed_tasks.clear()
|
||||
# when
|
||||
with patch(
|
||||
"allianceauth.eveonline.tasks.EveCharacter.objects.update_character"
|
||||
) as mock_update:
|
||||
mock_update.side_effect = RuntimeError
|
||||
update_character.delay(1)
|
||||
# then
|
||||
self.assertEqual(succeeded_tasks.count(), 0)
|
||||
self.assertEqual(retried_tasks.count(), 0)
|
||||
self.assertEqual(failed_tasks.count(), 1)
|
||||
|
||||
def test_should_reset_counters(self):
|
||||
# given
|
||||
succeeded_tasks.add()
|
||||
retried_tasks.add()
|
||||
failed_tasks.add()
|
||||
# when
|
||||
reset_counters()
|
||||
# then
|
||||
self.assertEqual(succeeded_tasks.count(), 0)
|
||||
self.assertEqual(retried_tasks.count(), 0)
|
||||
self.assertEqual(failed_tasks.count(), 0)
|
||||
|
||||
|
||||
class TestIsEnabled(TestCase):
|
||||
@override_settings(ALLIANCEAUTH_DASHBOARD_TASK_STATISTICS_DISABLED=False)
|
||||
def test_enabled(self):
|
||||
self.assertTrue(is_enabled())
|
||||
|
||||
@override_settings(ALLIANCEAUTH_DASHBOARD_TASK_STATISTICS_DISABLED=True)
|
||||
def test_disabled(self):
|
||||
self.assertFalse(is_enabled())
|
||||
@@ -1,3 +1,4 @@
|
||||
from bs4 import BeautifulSoup
|
||||
from urllib.parse import quote
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
@@ -188,7 +189,7 @@ class TestCaseWithTestData(TestCase):
|
||||
corporation_id=5432,
|
||||
corporation_name="Xavier's School for Gifted Youngsters",
|
||||
corporation_ticker='MUTNT',
|
||||
alliance_id = None,
|
||||
alliance_id=None,
|
||||
faction_id=999,
|
||||
faction_name='The X-Men',
|
||||
)
|
||||
@@ -206,6 +207,7 @@ class TestCaseWithTestData(TestCase):
|
||||
cls.user_4.profile.save()
|
||||
EveFactionInfo.objects.create(faction_id=999, faction_name='The X-Men')
|
||||
|
||||
|
||||
def make_generic_search_request(ModelClass: type, search_term: str):
|
||||
User.objects.create_superuser(
|
||||
username='superuser', password='secret', email='admin@example.com'
|
||||
@@ -218,6 +220,7 @@ def make_generic_search_request(ModelClass: type, search_term: str):
|
||||
|
||||
|
||||
class TestCharacterOwnershipAdmin(TestCaseWithTestData):
|
||||
fixtures = ["disable_analytics"]
|
||||
|
||||
def setUp(self):
|
||||
self.modeladmin = CharacterOwnershipAdmin(
|
||||
@@ -244,6 +247,7 @@ class TestCharacterOwnershipAdmin(TestCaseWithTestData):
|
||||
|
||||
|
||||
class TestOwnershipRecordAdmin(TestCaseWithTestData):
|
||||
fixtures = ["disable_analytics"]
|
||||
|
||||
def setUp(self):
|
||||
self.modeladmin = OwnershipRecordAdmin(
|
||||
@@ -270,6 +274,7 @@ class TestOwnershipRecordAdmin(TestCaseWithTestData):
|
||||
|
||||
|
||||
class TestStateAdmin(TestCaseWithTestData):
|
||||
fixtures = ["disable_analytics"]
|
||||
|
||||
def setUp(self):
|
||||
self.modeladmin = StateAdmin(
|
||||
@@ -299,6 +304,7 @@ class TestStateAdmin(TestCaseWithTestData):
|
||||
|
||||
|
||||
class TestUserAdmin(TestCaseWithTestData):
|
||||
fixtures = ["disable_analytics"]
|
||||
|
||||
def setUp(self):
|
||||
self.factory = RequestFactory()
|
||||
@@ -344,7 +350,7 @@ class TestUserAdmin(TestCaseWithTestData):
|
||||
self.assertEqual(user_main_organization(self.user_3), expected)
|
||||
|
||||
def test_user_main_organization_u4(self):
|
||||
expected="Xavier's School for Gifted Youngsters<br>The X-Men"
|
||||
expected = "Xavier's School for Gifted Youngsters<br>The X-Men"
|
||||
self.assertEqual(user_main_organization(self.user_4), expected)
|
||||
|
||||
def test_characters_u1(self):
|
||||
@@ -537,6 +543,42 @@ class TestUserAdmin(TestCaseWithTestData):
|
||||
self.assertEqual(response.status_code, expected)
|
||||
|
||||
|
||||
class TestUserAdminChangeForm(TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
super().setUpClass()
|
||||
cls.modeladmin = UserAdmin(model=User, admin_site=AdminSite())
|
||||
|
||||
def test_should_show_groups_available_to_user_with_blue_state_only(self):
|
||||
# given
|
||||
superuser = User.objects.create_superuser("Super")
|
||||
user = AuthUtils.create_user("Bruce Wayne")
|
||||
character = AuthUtils.add_main_character_2(
|
||||
user,
|
||||
name="Bruce Wayne",
|
||||
character_id=1001,
|
||||
corp_id=2001,
|
||||
corp_name="Wayne Technologies"
|
||||
)
|
||||
blue_state = State.objects.get(name="Blue")
|
||||
blue_state.member_characters.add(character)
|
||||
member_state = AuthUtils.get_member_state()
|
||||
group_1 = Group.objects.create(name="Group 1")
|
||||
group_2 = Group.objects.create(name="Group 2")
|
||||
group_2.authgroup.states.add(blue_state)
|
||||
group_3 = Group.objects.create(name="Group 3")
|
||||
group_3.authgroup.states.add(member_state)
|
||||
self.client.force_login(superuser)
|
||||
# when
|
||||
response = self.client.get(f"/admin/authentication/user/{user.pk}/change/")
|
||||
# then
|
||||
self.assertEqual(response.status_code, 200)
|
||||
soup = BeautifulSoup(response.rendered_content, features="html.parser")
|
||||
groups_select = soup.find("select", {"id": "id_groups"}).find_all('option')
|
||||
group_ids = {int(option["value"]) for option in groups_select}
|
||||
self.assertSetEqual(group_ids, {group_1.pk, group_2.pk})
|
||||
|
||||
|
||||
class TestMakeServicesHooksActions(TestCaseWithTestData):
|
||||
|
||||
class MyServicesHookTypeA(ServicesHook):
|
||||
|
||||
@@ -55,7 +55,6 @@ TEST_VERSION = '2.6.5'
|
||||
|
||||
|
||||
class TestStatusOverviewTag(TestCase):
|
||||
|
||||
@patch(MODULE_PATH + '.admin_status.__version__', TEST_VERSION)
|
||||
@patch(MODULE_PATH + '.admin_status._fetch_celery_queue_length')
|
||||
@patch(MODULE_PATH + '.admin_status._current_version_summary')
|
||||
@@ -66,6 +65,7 @@ class TestStatusOverviewTag(TestCase):
|
||||
mock_current_version_info,
|
||||
mock_fetch_celery_queue_length
|
||||
):
|
||||
# given
|
||||
notifications = {
|
||||
'notifications': GITHUB_NOTIFICATION_ISSUES[:5]
|
||||
}
|
||||
@@ -83,22 +83,20 @@ class TestStatusOverviewTag(TestCase):
|
||||
}
|
||||
mock_current_version_info.return_value = version_info
|
||||
mock_fetch_celery_queue_length.return_value = 3
|
||||
|
||||
# when
|
||||
result = status_overview()
|
||||
expected = {
|
||||
'notifications': GITHUB_NOTIFICATION_ISSUES[:5],
|
||||
'latest_major': True,
|
||||
'latest_minor': True,
|
||||
'latest_patch': True,
|
||||
'latest_beta': False,
|
||||
'current_version': TEST_VERSION,
|
||||
'latest_major_version': '2.4.5',
|
||||
'latest_minor_version': '2.4.0',
|
||||
'latest_patch_version': '2.4.5',
|
||||
'latest_beta_version': '2.4.4a1',
|
||||
'task_queue_length': 3,
|
||||
}
|
||||
self.assertEqual(result, expected)
|
||||
# then
|
||||
self.assertEqual(result["notifications"], GITHUB_NOTIFICATION_ISSUES[:5])
|
||||
self.assertTrue(result["latest_major"])
|
||||
self.assertTrue(result["latest_minor"])
|
||||
self.assertTrue(result["latest_patch"])
|
||||
self.assertFalse(result["latest_beta"])
|
||||
self.assertEqual(result["current_version"], TEST_VERSION)
|
||||
self.assertEqual(result["latest_major_version"], '2.4.5')
|
||||
self.assertEqual(result["latest_minor_version"], '2.4.0')
|
||||
self.assertEqual(result["latest_patch_version"], '2.4.5')
|
||||
self.assertEqual(result["latest_beta_version"], '2.4.4a1')
|
||||
self.assertEqual(result["task_queue_length"], 3)
|
||||
|
||||
|
||||
class TestNotifications(TestCase):
|
||||
|
||||
@@ -193,6 +193,8 @@
|
||||
"columnDefs": [
|
||||
{ "sortable": false, "targets": [1] },
|
||||
],
|
||||
"stateSave": true,
|
||||
"stateDuration": 0
|
||||
});
|
||||
$('#table-members').DataTable({
|
||||
"columnDefs": [
|
||||
@@ -200,6 +202,8 @@
|
||||
{ "sortable": false, "targets": [0, 2] },
|
||||
],
|
||||
"order": [[ 1, "asc" ]],
|
||||
"stateSave": true,
|
||||
"stateDuration": 0
|
||||
});
|
||||
$('#table-unregistered').DataTable({
|
||||
"columnDefs": [
|
||||
@@ -207,6 +211,8 @@
|
||||
{ "sortable": false, "targets": [0, 2] },
|
||||
],
|
||||
"order": [[ 1, "asc" ]],
|
||||
"stateSave": true,
|
||||
"stateDuration": 0
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -43,6 +43,9 @@
|
||||
{% endblock %}
|
||||
{% block extra_script %}
|
||||
$(document).ready(function(){
|
||||
$('#table-search').DataTable();
|
||||
$('#table-search').DataTable({
|
||||
"stateSave": true,
|
||||
"stateDuration": 0
|
||||
});
|
||||
});
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,13 +1,27 @@
|
||||
from django.db import models
|
||||
import logging
|
||||
from typing import Union
|
||||
|
||||
from .managers import EveCharacterManager, EveCharacterProviderManager
|
||||
from .managers import EveCorporationManager, EveCorporationProviderManager
|
||||
from .managers import EveAllianceManager, EveAllianceProviderManager
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db import models
|
||||
from esi.models import Token
|
||||
|
||||
from allianceauth.notifications import notify
|
||||
|
||||
from . import providers
|
||||
from .evelinks import eveimageserver
|
||||
from .managers import (
|
||||
EveAllianceManager,
|
||||
EveAllianceProviderManager,
|
||||
EveCharacterManager,
|
||||
EveCharacterProviderManager,
|
||||
EveCorporationManager,
|
||||
EveCorporationProviderManager,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_DEFAULT_IMAGE_SIZE = 32
|
||||
DOOMHEIM_CORPORATION_ID = 1000001
|
||||
|
||||
|
||||
class EveFactionInfo(models.Model):
|
||||
@@ -68,13 +82,12 @@ class EveAllianceInfo(models.Model):
|
||||
for corp_id in alliance.corp_ids:
|
||||
if not EveCorporationInfo.objects.filter(corporation_id=corp_id).exists():
|
||||
EveCorporationInfo.objects.create_corporation(corp_id)
|
||||
EveCorporationInfo.objects.filter(
|
||||
corporation_id__in=alliance.corp_ids).update(alliance=self
|
||||
EveCorporationInfo.objects.filter(corporation_id__in=alliance.corp_ids).update(
|
||||
alliance=self
|
||||
)
|
||||
EveCorporationInfo.objects\
|
||||
.filter(alliance=self)\
|
||||
.exclude(corporation_id__in=alliance.corp_ids)\
|
||||
.update(alliance=None)
|
||||
EveCorporationInfo.objects.filter(alliance=self).exclude(
|
||||
corporation_id__in=alliance.corp_ids
|
||||
).update(alliance=None)
|
||||
|
||||
def update_alliance(self, alliance: providers.Alliance = None):
|
||||
if alliance is None:
|
||||
@@ -182,6 +195,7 @@ class EveCorporationInfo(models.Model):
|
||||
|
||||
|
||||
class EveCharacter(models.Model):
|
||||
"""Character in Eve Online"""
|
||||
character_id = models.PositiveIntegerField(unique=True)
|
||||
character_name = models.CharField(max_length=254, unique=True)
|
||||
corporation_id = models.PositiveIntegerField()
|
||||
@@ -198,12 +212,20 @@ class EveCharacter(models.Model):
|
||||
|
||||
class Meta:
|
||||
indexes = [
|
||||
models.Index(fields=['corporation_id',]),
|
||||
models.Index(fields=['alliance_id',]),
|
||||
models.Index(fields=['corporation_name',]),
|
||||
models.Index(fields=['alliance_name',]),
|
||||
models.Index(fields=['faction_id',]),
|
||||
]
|
||||
models.Index(fields=['corporation_id',]),
|
||||
models.Index(fields=['alliance_id',]),
|
||||
models.Index(fields=['corporation_name',]),
|
||||
models.Index(fields=['alliance_name',]),
|
||||
models.Index(fields=['faction_id',]),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return self.character_name
|
||||
|
||||
@property
|
||||
def is_biomassed(self) -> bool:
|
||||
"""Whether this character is dead or not."""
|
||||
return self.corporation_id == DOOMHEIM_CORPORATION_ID
|
||||
|
||||
@property
|
||||
def alliance(self) -> Union[EveAllianceInfo, None]:
|
||||
@@ -249,10 +271,36 @@ class EveCharacter(models.Model):
|
||||
self.faction_id = character.faction.id
|
||||
self.faction_name = character.faction.name
|
||||
self.save()
|
||||
if self.is_biomassed:
|
||||
self._remove_tokens_of_biomassed_character()
|
||||
return self
|
||||
|
||||
def __str__(self):
|
||||
return self.character_name
|
||||
def _remove_tokens_of_biomassed_character(self) -> None:
|
||||
"""Remove tokens of this biomassed character."""
|
||||
try:
|
||||
user = self.character_ownership.user
|
||||
except ObjectDoesNotExist:
|
||||
return
|
||||
tokens_to_delete = Token.objects.filter(character_id=self.character_id)
|
||||
tokens_count = tokens_to_delete.count()
|
||||
if not tokens_count:
|
||||
return
|
||||
tokens_to_delete.delete()
|
||||
logger.info(
|
||||
"%d tokens from user %s for biomassed character %s [id:%s] deleted.",
|
||||
tokens_count,
|
||||
user,
|
||||
self,
|
||||
self.character_id,
|
||||
)
|
||||
notify(
|
||||
user=user,
|
||||
title=f"Character {self} biomassed",
|
||||
message=(
|
||||
f"Your former character {self} has been biomassed "
|
||||
"and has been removed from the list of your alts."
|
||||
)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def generic_portrait_url(
|
||||
@@ -336,7 +384,6 @@ class EveCharacter(models.Model):
|
||||
"""image URL for alliance of this character or empty string"""
|
||||
return self.alliance_logo_url(256)
|
||||
|
||||
|
||||
def faction_logo_url(self, size=_DEFAULT_IMAGE_SIZE) -> str:
|
||||
"""image URL for alliance of this character or empty string"""
|
||||
if self.faction_id:
|
||||
|
||||
@@ -13,17 +13,18 @@ from allianceauth import __version__
|
||||
SWAGGER_SPEC_PATH = os.path.join(os.path.dirname(
|
||||
os.path.abspath(__file__)), 'swagger.json'
|
||||
)
|
||||
"""
|
||||
Swagger spec operations:
|
||||
|
||||
get_alliances_alliance_id
|
||||
get_alliances_alliance_id_corporations
|
||||
get_corporations_corporation_id
|
||||
get_characters_character_id
|
||||
get_universe_types_type_id
|
||||
post_character_affiliation
|
||||
get_universe_factions
|
||||
"""
|
||||
# for the love of Bob please add operations you use here. I'm tired of breaking undocumented things.
|
||||
ESI_OPERATIONS=[
|
||||
'get_alliances_alliance_id',
|
||||
'get_alliances_alliance_id_corporations',
|
||||
'get_corporations_corporation_id',
|
||||
'get_characters_character_id',
|
||||
'post_characters_affiliation',
|
||||
'get_universe_types_type_id',
|
||||
'get_universe_factions',
|
||||
'post_universe_names',
|
||||
]
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -169,7 +170,7 @@ class EveProvider:
|
||||
"""
|
||||
:return: an ItemType object for the given ID
|
||||
"""
|
||||
raise NotImplemented()
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class EveSwaggerProvider(EveProvider):
|
||||
@@ -206,7 +207,8 @@ class EveSwaggerProvider(EveProvider):
|
||||
def __str__(self):
|
||||
return 'esi'
|
||||
|
||||
def get_alliance(self, alliance_id):
|
||||
def get_alliance(self, alliance_id: int) -> Alliance:
|
||||
"""Fetch alliance from ESI."""
|
||||
try:
|
||||
data = self.client.Alliance.get_alliances_alliance_id(alliance_id=alliance_id).result()
|
||||
corps = self.client.Alliance.get_alliances_alliance_id_corporations(alliance_id=alliance_id).result()
|
||||
@@ -222,7 +224,8 @@ class EveSwaggerProvider(EveProvider):
|
||||
except HTTPNotFound:
|
||||
raise ObjectNotFound(alliance_id, 'alliance')
|
||||
|
||||
def get_corp(self, corp_id):
|
||||
def get_corp(self, corp_id: int) -> Corporation:
|
||||
"""Fetch corporation from ESI."""
|
||||
try:
|
||||
data = self.client.Corporation.get_corporations_corporation_id(corporation_id=corp_id).result()
|
||||
model = Corporation(
|
||||
@@ -238,29 +241,43 @@ class EveSwaggerProvider(EveProvider):
|
||||
except HTTPNotFound:
|
||||
raise ObjectNotFound(corp_id, 'corporation')
|
||||
|
||||
def get_character(self, character_id):
|
||||
def get_character(self, character_id: int) -> Character:
|
||||
"""Fetch character from ESI."""
|
||||
try:
|
||||
data = self.client.Character.get_characters_character_id(character_id=character_id).result()
|
||||
character_name = self._fetch_character_name(character_id)
|
||||
affiliation = self.client.Character.post_characters_affiliation(characters=[character_id]).result()[0]
|
||||
|
||||
model = Character(
|
||||
id=character_id,
|
||||
name=data['name'],
|
||||
name=character_name,
|
||||
corp_id=affiliation['corporation_id'],
|
||||
alliance_id=affiliation['alliance_id'] if 'alliance_id' in affiliation else None,
|
||||
faction_id=affiliation['faction_id'] if 'faction_id' in affiliation else None,
|
||||
)
|
||||
return model
|
||||
except (HTTPNotFound, HTTPUnprocessableEntity):
|
||||
except (HTTPNotFound, HTTPUnprocessableEntity, ObjectNotFound):
|
||||
raise ObjectNotFound(character_id, 'character')
|
||||
|
||||
def _fetch_character_name(self, character_id: int) -> str:
|
||||
"""Fetch character name from ESI."""
|
||||
data = self.client.Universe.post_universe_names(ids=[character_id]).result()
|
||||
character = data.pop() if data else None
|
||||
if (
|
||||
not character
|
||||
or character["category"] != "character"
|
||||
or character["id"] != character_id
|
||||
):
|
||||
raise ObjectNotFound(character_id, 'character')
|
||||
return character["name"]
|
||||
|
||||
def get_all_factions(self):
|
||||
"""Fetch all factions from ESI."""
|
||||
if not self._faction_list:
|
||||
self._faction_list = self.client.Universe.get_universe_factions().result()
|
||||
return self._faction_list
|
||||
|
||||
def get_faction(self, faction_id):
|
||||
faction_id=int(faction_id)
|
||||
def get_faction(self, faction_id: int):
|
||||
"""Fetch faction from ESI."""
|
||||
faction_id = int(faction_id)
|
||||
try:
|
||||
if not self._faction_list:
|
||||
_ = self.get_all_factions()
|
||||
@@ -272,7 +289,8 @@ class EveSwaggerProvider(EveProvider):
|
||||
except (HTTPNotFound, HTTPUnprocessableEntity, KeyError):
|
||||
raise ObjectNotFound(faction_id, 'faction')
|
||||
|
||||
def get_itemtype(self, type_id):
|
||||
def get_itemtype(self, type_id: int) -> ItemType:
|
||||
"""Fetch inventory item from ESI."""
|
||||
try:
|
||||
data = self.client.Universe.get_universe_types_type_id(type_id=type_id).result()
|
||||
return ItemType(id=type_id, name=data['name'])
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,12 +1,11 @@
|
||||
import logging
|
||||
|
||||
from celery import shared_task
|
||||
from .models import EveAllianceInfo
|
||||
from .models import EveCharacter
|
||||
from .models import EveCorporationInfo
|
||||
|
||||
from .models import EveAllianceInfo, EveCharacter, EveCorporationInfo
|
||||
from . import providers
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
TASK_PRIORITY = 7
|
||||
@@ -32,8 +31,8 @@ def update_alliance(alliance_id):
|
||||
|
||||
|
||||
@shared_task
|
||||
def update_character(character_id):
|
||||
"""Update given character from ESI"""
|
||||
def update_character(character_id: int) -> None:
|
||||
"""Update given character from ESI."""
|
||||
EveCharacter.objects.update_character(character_id)
|
||||
|
||||
|
||||
@@ -65,17 +64,17 @@ def update_character_chunk(character_ids_chunk: list):
|
||||
.post_characters_affiliation(characters=character_ids_chunk).result()
|
||||
character_names = providers.provider.client.Universe\
|
||||
.post_universe_names(ids=character_ids_chunk).result()
|
||||
except:
|
||||
except OSError:
|
||||
logger.info("Failed to bulk update characters. Attempting single updates")
|
||||
for character_id in character_ids_chunk:
|
||||
update_character.apply_async(
|
||||
args=[character_id], priority=TASK_PRIORITY
|
||||
)
|
||||
args=[character_id], priority=TASK_PRIORITY
|
||||
)
|
||||
return
|
||||
|
||||
affiliations = {
|
||||
affiliation.get('character_id'): affiliation
|
||||
for affiliation in affiliations_raw
|
||||
affiliation.get('character_id'): affiliation
|
||||
for affiliation in affiliations_raw
|
||||
}
|
||||
# add character names to affiliations
|
||||
for character in character_names:
|
||||
@@ -108,5 +107,5 @@ def update_character_chunk(character_ids_chunk: list):
|
||||
|
||||
if corp_changed or alliance_changed or name_changed:
|
||||
update_character.apply_async(
|
||||
args=[character.get('character_id')], priority=TASK_PRIORITY
|
||||
)
|
||||
args=[character.get('character_id')], priority=TASK_PRIORITY
|
||||
)
|
||||
|
||||
168
allianceauth/eveonline/tests/esi_client_stub.py
Normal file
168
allianceauth/eveonline/tests/esi_client_stub.py
Normal file
@@ -0,0 +1,168 @@
|
||||
from bravado.exception import HTTPNotFound
|
||||
|
||||
|
||||
class BravadoResponseStub:
|
||||
"""Stub for IncomingResponse in bravado, e.g. for HTTPError exceptions"""
|
||||
|
||||
def __init__(
|
||||
self, status_code, reason="", text="", headers=None, raw_bytes=None
|
||||
) -> None:
|
||||
self.reason = reason
|
||||
self.status_code = status_code
|
||||
self.text = text
|
||||
self.headers = headers if headers else dict()
|
||||
self.raw_bytes = raw_bytes
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.status_code} {self.reason}"
|
||||
|
||||
|
||||
class BravadoOperationStub:
|
||||
"""Stub to simulate the operation object return from bravado via django-esi"""
|
||||
|
||||
class RequestConfig:
|
||||
def __init__(self, also_return_response):
|
||||
self.also_return_response = also_return_response
|
||||
|
||||
class ResponseStub:
|
||||
def __init__(self, headers):
|
||||
self.headers = headers
|
||||
|
||||
def __init__(self, data, headers: dict = None, also_return_response: bool = False):
|
||||
self._data = data
|
||||
self._headers = headers if headers else {"x-pages": 1}
|
||||
self.request_config = BravadoOperationStub.RequestConfig(also_return_response)
|
||||
|
||||
def result(self, **kwargs):
|
||||
if self.request_config.also_return_response:
|
||||
return [self._data, self.ResponseStub(self._headers)]
|
||||
else:
|
||||
return self._data
|
||||
|
||||
def results(self, **kwargs):
|
||||
return self.result(**kwargs)
|
||||
|
||||
|
||||
class EsiClientStub:
|
||||
"""Stub for an ESI client."""
|
||||
class Alliance:
|
||||
@staticmethod
|
||||
def get_alliances_alliance_id(alliance_id):
|
||||
data = {
|
||||
3001: {
|
||||
"name": "Wayne Enterprises",
|
||||
"ticker": "WYE",
|
||||
"executor_corporation_id": 2001
|
||||
}
|
||||
}
|
||||
try:
|
||||
return BravadoOperationStub(data[int(alliance_id)])
|
||||
except KeyError:
|
||||
response = BravadoResponseStub(
|
||||
404, f"Alliance with ID {alliance_id} not found"
|
||||
)
|
||||
raise HTTPNotFound(response)
|
||||
|
||||
@staticmethod
|
||||
def get_alliances_alliance_id_corporations(alliance_id):
|
||||
data = [2001, 2002, 2003]
|
||||
return BravadoOperationStub(data)
|
||||
|
||||
class Character:
|
||||
@staticmethod
|
||||
def get_characters_character_id(character_id):
|
||||
data = {
|
||||
1001: {
|
||||
"corporation_id": 2001,
|
||||
"name": "Bruce Wayne",
|
||||
},
|
||||
1002: {
|
||||
"corporation_id": 2001,
|
||||
"name": "Peter Parker",
|
||||
},
|
||||
1011: {
|
||||
"corporation_id": 2011,
|
||||
"name": "Lex Luthor",
|
||||
}
|
||||
}
|
||||
try:
|
||||
return BravadoOperationStub(data[int(character_id)])
|
||||
except KeyError:
|
||||
response = BravadoResponseStub(
|
||||
404, f"Character with ID {character_id} not found"
|
||||
)
|
||||
raise HTTPNotFound(response)
|
||||
|
||||
@staticmethod
|
||||
def post_characters_affiliation(characters: list):
|
||||
data = [
|
||||
{'character_id': 1001, 'corporation_id': 2001, 'alliance_id': 3001},
|
||||
{'character_id': 1002, 'corporation_id': 2001, 'alliance_id': 3001},
|
||||
{'character_id': 1011, 'corporation_id': 2011},
|
||||
{'character_id': 1666, 'corporation_id': 1000001},
|
||||
]
|
||||
return BravadoOperationStub(
|
||||
[x for x in data if x['character_id'] in characters]
|
||||
)
|
||||
|
||||
class Corporation:
|
||||
@staticmethod
|
||||
def get_corporations_corporation_id(corporation_id):
|
||||
data = {
|
||||
2001: {
|
||||
"ceo_id": 1091,
|
||||
"member_count": 10,
|
||||
"name": "Wayne Technologies",
|
||||
"ticker": "WTE",
|
||||
"alliance_id": 3001
|
||||
},
|
||||
2002: {
|
||||
"ceo_id": 1092,
|
||||
"member_count": 10,
|
||||
"name": "Wayne Food",
|
||||
"ticker": "WFO",
|
||||
"alliance_id": 3001
|
||||
},
|
||||
2003: {
|
||||
"ceo_id": 1093,
|
||||
"member_count": 10,
|
||||
"name": "Wayne Energy",
|
||||
"ticker": "WEG",
|
||||
"alliance_id": 3001
|
||||
},
|
||||
2011: {
|
||||
"ceo_id": 1,
|
||||
"member_count": 3,
|
||||
"name": "LexCorp",
|
||||
"ticker": "LC",
|
||||
},
|
||||
1000001: {
|
||||
"ceo_id": 3000001,
|
||||
"creator_id": 1,
|
||||
"description": "The internal corporation used for characters in graveyard.",
|
||||
"member_count": 6329026,
|
||||
"name": "Doomheim",
|
||||
"ticker": "666",
|
||||
}
|
||||
}
|
||||
try:
|
||||
return BravadoOperationStub(data[int(corporation_id)])
|
||||
except KeyError:
|
||||
response = BravadoResponseStub(
|
||||
404, f"Corporation with ID {corporation_id} not found"
|
||||
)
|
||||
raise HTTPNotFound(response)
|
||||
|
||||
class Universe:
|
||||
@staticmethod
|
||||
def post_universe_names(ids: list):
|
||||
data = [
|
||||
{"category": "character", "id": 1001, "name": "Bruce Wayne"},
|
||||
{"category": "character", "id": 1002, "name": "Peter Parker"},
|
||||
{"category": "character", "id": 1011, "name": "Lex Luthor"},
|
||||
{"category": "character", "id": 1666, "name": "Hal Jordan"},
|
||||
{"category": "corporation", "id": 2001, "name": "Wayne Technologies"},
|
||||
{"category": "corporation","id": 2002, "name": "Wayne Food"},
|
||||
{"category": "corporation","id": 1000001, "name": "Doomheim"},
|
||||
]
|
||||
return BravadoOperationStub([x for x in data if x['id'] in ids])
|
||||
@@ -1,12 +1,15 @@
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.test import TestCase
|
||||
from esi.models import Token
|
||||
|
||||
from allianceauth.tests.auth_utils import AuthUtils
|
||||
|
||||
from ..models import (
|
||||
EveCharacter, EveCorporationInfo, EveAllianceInfo, EveFactionInfo
|
||||
)
|
||||
from ..providers import Alliance, Corporation, Character
|
||||
from ..evelinks import eveimageserver
|
||||
from ..models import EveAllianceInfo, EveCharacter, EveCorporationInfo, EveFactionInfo
|
||||
from ..providers import Alliance, Character, Corporation
|
||||
from .esi_client_stub import EsiClientStub
|
||||
|
||||
|
||||
class EveCharacterTestCase(TestCase):
|
||||
@@ -402,8 +405,8 @@ class EveAllianceTestCase(TestCase):
|
||||
my_alliance.save()
|
||||
my_alliance.populate_alliance()
|
||||
|
||||
for corporation in EveCorporationInfo.objects\
|
||||
.filter(corporation_id__in=[2001, 2002]
|
||||
for corporation in (
|
||||
EveCorporationInfo.objects.filter(corporation_id__in=[2001, 2002])
|
||||
):
|
||||
self.assertEqual(corporation.alliance, my_alliance)
|
||||
|
||||
@@ -587,3 +590,98 @@ class EveCorporationTestCase(TestCase):
|
||||
self.my_corp.logo_url_256,
|
||||
'https://images.evetech.net/corporations/2001/logo?size=256'
|
||||
)
|
||||
|
||||
|
||||
@patch('allianceauth.eveonline.providers.esi_client_factory')
|
||||
@patch("allianceauth.eveonline.models.notify")
|
||||
class TestCharacterUpdate(TestCase):
|
||||
def test_should_update_normal_character(self, mock_notify, mock_esi_client_factory):
|
||||
# given
|
||||
mock_esi_client_factory.return_value = EsiClientStub()
|
||||
my_character = EveCharacter.objects.create(
|
||||
character_id=1001,
|
||||
character_name="not my name",
|
||||
corporation_id=2002,
|
||||
corporation_name="Wayne Food",
|
||||
corporation_ticker="WYF",
|
||||
alliance_id=None
|
||||
)
|
||||
# when
|
||||
my_character.update_character()
|
||||
# then
|
||||
my_character.refresh_from_db()
|
||||
self.assertEqual(my_character.character_name, "Bruce Wayne")
|
||||
self.assertEqual(my_character.corporation_id, 2001)
|
||||
self.assertEqual(my_character.corporation_name, "Wayne Technologies")
|
||||
self.assertEqual(my_character.corporation_ticker, "WTE")
|
||||
self.assertEqual(my_character.alliance_id, 3001)
|
||||
self.assertEqual(my_character.alliance_name, "Wayne Enterprises")
|
||||
self.assertEqual(my_character.alliance_ticker, "WYE")
|
||||
self.assertFalse(mock_notify.called)
|
||||
|
||||
def test_should_update_dead_character_with_owner(
|
||||
self, mock_notify, mock_esi_client_factory
|
||||
):
|
||||
# given
|
||||
mock_esi_client_factory.return_value = EsiClientStub()
|
||||
character_1666 = EveCharacter.objects.create(
|
||||
character_id=1666,
|
||||
character_name="Hal Jordan",
|
||||
corporation_id=2002,
|
||||
corporation_name="Wayne Food",
|
||||
corporation_ticker="WYF",
|
||||
alliance_id=None
|
||||
)
|
||||
user = AuthUtils.create_user("Bruce Wayne")
|
||||
token_1666 = Token.objects.create(
|
||||
user=user,
|
||||
character_id=character_1666.character_id,
|
||||
character_name=character_1666.character_name,
|
||||
character_owner_hash="ABC123-1666",
|
||||
)
|
||||
character_1001 = EveCharacter.objects.create(
|
||||
character_id=1001,
|
||||
character_name="Bruce Wayne",
|
||||
corporation_id=2001,
|
||||
corporation_name="Wayne Technologies",
|
||||
corporation_ticker="WYT",
|
||||
alliance_id=None
|
||||
)
|
||||
token_1001 = Token.objects.create(
|
||||
user=user,
|
||||
character_id=character_1001.character_id,
|
||||
character_name=character_1001.character_name,
|
||||
character_owner_hash="ABC123-1001",
|
||||
)
|
||||
# when
|
||||
character_1666.update_character()
|
||||
# then
|
||||
character_1666.refresh_from_db()
|
||||
self.assertTrue(character_1666.is_biomassed)
|
||||
self.assertNotIn(token_1666, user.token_set.all())
|
||||
self.assertIn(token_1001, user.token_set.all())
|
||||
with self.assertRaises(ObjectDoesNotExist):
|
||||
self.assertTrue(character_1666.character_ownership)
|
||||
user.profile.refresh_from_db()
|
||||
self.assertIsNone(user.profile.main_character)
|
||||
self.assertTrue(mock_notify.called)
|
||||
|
||||
def test_should_handle_dead_character_without_owner(
|
||||
self, mock_notify, mock_esi_client_factory
|
||||
):
|
||||
# given
|
||||
mock_esi_client_factory.return_value = EsiClientStub()
|
||||
character_1666 = EveCharacter.objects.create(
|
||||
character_id=1666,
|
||||
character_name="Hal Jordan",
|
||||
corporation_id=1011,
|
||||
corporation_name="LexCorp",
|
||||
corporation_ticker='LC',
|
||||
alliance_id=None
|
||||
)
|
||||
# when
|
||||
character_1666.update_character()
|
||||
# then
|
||||
character_1666.refresh_from_db()
|
||||
self.assertTrue(character_1666.is_biomassed)
|
||||
self.assertFalse(mock_notify.called)
|
||||
|
||||
@@ -7,6 +7,7 @@ from jsonschema.exceptions import RefResolutionError
|
||||
from django.test import TestCase
|
||||
|
||||
from . import set_logger
|
||||
from .esi_client_stub import EsiClientStub
|
||||
from ..providers import (
|
||||
ObjectNotFound,
|
||||
Entity,
|
||||
@@ -632,13 +633,7 @@ class TestEveSwaggerProvider(TestCase):
|
||||
|
||||
@patch(MODULE_PATH + '.esi_client_factory')
|
||||
def test_get_character(self, mock_esi_client_factory):
|
||||
mock_esi_client_factory.return_value \
|
||||
.Character.get_characters_character_id \
|
||||
= TestEveSwaggerProvider.esi_get_characters_character_id
|
||||
mock_esi_client_factory.return_value \
|
||||
.Character.post_characters_affiliation \
|
||||
= TestEveSwaggerProvider.esi_post_characters_affiliation
|
||||
|
||||
mock_esi_client_factory.return_value = EsiClientStub()
|
||||
my_provider = EveSwaggerProvider()
|
||||
|
||||
# character with alliance
|
||||
@@ -649,8 +644,8 @@ class TestEveSwaggerProvider(TestCase):
|
||||
self.assertEqual(my_character.alliance_id, 3001)
|
||||
|
||||
# character wo/ alliance
|
||||
my_character = my_provider.get_character(1002)
|
||||
self.assertEqual(my_character.id, 1002)
|
||||
my_character = my_provider.get_character(1011)
|
||||
self.assertEqual(my_character.id, 1011)
|
||||
self.assertEqual(my_character.alliance_id, None)
|
||||
|
||||
# character not found
|
||||
|
||||
@@ -1,245 +1,271 @@
|
||||
from unittest.mock import patch, Mock
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.test import TestCase
|
||||
from django.test import TestCase, TransactionTestCase, override_settings
|
||||
|
||||
from ..models import EveCharacter, EveCorporationInfo, EveAllianceInfo
|
||||
from ..models import EveAllianceInfo, EveCharacter, EveCorporationInfo
|
||||
from ..tasks import (
|
||||
run_model_update,
|
||||
update_alliance,
|
||||
update_corp,
|
||||
update_character,
|
||||
run_model_update
|
||||
update_character_chunk,
|
||||
update_corp,
|
||||
)
|
||||
from .esi_client_stub import EsiClientStub
|
||||
|
||||
|
||||
class TestTasks(TestCase):
|
||||
|
||||
@patch('allianceauth.eveonline.tasks.EveCorporationInfo')
|
||||
def test_update_corp(self, mock_EveCorporationInfo):
|
||||
update_corp(42)
|
||||
self.assertEqual(
|
||||
mock_EveCorporationInfo.objects.update_corporation.call_count, 1
|
||||
)
|
||||
self.assertEqual(
|
||||
mock_EveCorporationInfo.objects.update_corporation.call_args[0][0], 42
|
||||
@patch('allianceauth.eveonline.providers.esi_client_factory')
|
||||
class TestUpdateTasks(TestCase):
|
||||
def test_should_update_alliance(self, mock_esi_client_factory):
|
||||
# given
|
||||
mock_esi_client_factory.return_value = EsiClientStub()
|
||||
my_alliance = EveAllianceInfo.objects.create(
|
||||
alliance_id=3001,
|
||||
alliance_name="Wayne Enterprises",
|
||||
alliance_ticker="WYE",
|
||||
executor_corp_id=2003
|
||||
)
|
||||
# when
|
||||
update_alliance(my_alliance.alliance_id)
|
||||
# then
|
||||
my_alliance.refresh_from_db()
|
||||
self.assertEqual(my_alliance.executor_corp_id, 2001)
|
||||
|
||||
@patch('allianceauth.eveonline.tasks.EveAllianceInfo')
|
||||
def test_update_alliance(self, mock_EveAllianceInfo):
|
||||
update_alliance(42)
|
||||
self.assertEqual(
|
||||
mock_EveAllianceInfo.objects.update_alliance.call_args[0][0], 42
|
||||
)
|
||||
self.assertEqual(
|
||||
mock_EveAllianceInfo.objects
|
||||
.update_alliance.return_value.populate_alliance.call_count, 1
|
||||
def test_should_update_character(self, mock_esi_client_factory):
|
||||
# given
|
||||
mock_esi_client_factory.return_value = EsiClientStub()
|
||||
my_character = EveCharacter.objects.create(
|
||||
character_id=1001,
|
||||
character_name="Bruce Wayne",
|
||||
corporation_id=2002,
|
||||
corporation_name="Wayne Food",
|
||||
corporation_ticker="WYF",
|
||||
alliance_id=None
|
||||
)
|
||||
# when
|
||||
update_character(my_character.character_id)
|
||||
# then
|
||||
my_character.refresh_from_db()
|
||||
self.assertEqual(my_character.corporation_id, 2001)
|
||||
|
||||
@patch('allianceauth.eveonline.tasks.EveCharacter')
|
||||
def test_update_character(self, mock_EveCharacter):
|
||||
update_character(42)
|
||||
self.assertEqual(
|
||||
mock_EveCharacter.objects.update_character.call_count, 1
|
||||
def test_should_update_corp(self, mock_esi_client_factory):
|
||||
# given
|
||||
mock_esi_client_factory.return_value = EsiClientStub()
|
||||
EveAllianceInfo.objects.create(
|
||||
alliance_id=3001,
|
||||
alliance_name="Wayne Enterprises",
|
||||
alliance_ticker="WYE",
|
||||
executor_corp_id=2003
|
||||
)
|
||||
self.assertEqual(
|
||||
mock_EveCharacter.objects.update_character.call_args[0][0], 42
|
||||
my_corporation = EveCorporationInfo.objects.create(
|
||||
corporation_id=2003,
|
||||
corporation_name="Wayne Food",
|
||||
corporation_ticker="WFO",
|
||||
member_count=1,
|
||||
alliance=None,
|
||||
ceo_id=1999
|
||||
)
|
||||
# when
|
||||
update_corp(my_corporation.corporation_id)
|
||||
# then
|
||||
my_corporation.refresh_from_db()
|
||||
self.assertEqual(my_corporation.alliance.alliance_id, 3001)
|
||||
|
||||
# @patch('allianceauth.eveonline.tasks.EveCharacter')
|
||||
# def test_update_character(self, mock_EveCharacter):
|
||||
# update_character(42)
|
||||
# self.assertEqual(
|
||||
# mock_EveCharacter.objects.update_character.call_count, 1
|
||||
# )
|
||||
# self.assertEqual(
|
||||
# mock_EveCharacter.objects.update_character.call_args[0][0], 42
|
||||
# )
|
||||
|
||||
|
||||
@patch('allianceauth.eveonline.tasks.update_character')
|
||||
@patch('allianceauth.eveonline.tasks.update_alliance')
|
||||
@patch('allianceauth.eveonline.tasks.update_corp')
|
||||
@patch('allianceauth.eveonline.providers.provider')
|
||||
@override_settings(CELERY_ALWAYS_EAGER=True)
|
||||
@patch('allianceauth.eveonline.providers.esi_client_factory')
|
||||
@patch('allianceauth.eveonline.tasks.providers')
|
||||
@patch('allianceauth.eveonline.tasks.CHUNK_SIZE', 2)
|
||||
class TestRunModelUpdate(TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
EveCorporationInfo.objects.all().delete()
|
||||
EveAllianceInfo.objects.all().delete()
|
||||
EveCharacter.objects.all().delete()
|
||||
|
||||
class TestRunModelUpdate(TransactionTestCase):
|
||||
def test_should_run_updates(self, mock_providers, mock_esi_client_factory):
|
||||
# given
|
||||
mock_providers.provider.client = EsiClientStub()
|
||||
mock_esi_client_factory.return_value = EsiClientStub()
|
||||
EveCorporationInfo.objects.create(
|
||||
corporation_id=2345,
|
||||
corporation_name='corp.name',
|
||||
corporation_ticker='c.c.t',
|
||||
corporation_id=2001,
|
||||
corporation_name="Wayne Technologies",
|
||||
corporation_ticker="WTE",
|
||||
member_count=10,
|
||||
alliance=None,
|
||||
)
|
||||
EveAllianceInfo.objects.create(
|
||||
alliance_id=3456,
|
||||
alliance_name='alliance.name',
|
||||
alliance_ticker='a.t',
|
||||
executor_corp_id=5,
|
||||
alliance_3001 = EveAllianceInfo.objects.create(
|
||||
alliance_id=3001,
|
||||
alliance_name="Wayne Enterprises",
|
||||
alliance_ticker="WYE",
|
||||
executor_corp_id=2003
|
||||
)
|
||||
EveCharacter.objects.create(
|
||||
character_id=1,
|
||||
character_name='character.name1',
|
||||
corporation_id=2345,
|
||||
corporation_name='character.corp.name',
|
||||
corporation_ticker='c.c.t', # max 5 chars
|
||||
corporation_2003 = EveCorporationInfo.objects.create(
|
||||
corporation_id=2003,
|
||||
corporation_name="Wayne Energy",
|
||||
corporation_ticker="WEG",
|
||||
member_count=99,
|
||||
alliance=None,
|
||||
)
|
||||
character_1001 = EveCharacter.objects.create(
|
||||
character_id=1001,
|
||||
character_name="Bruce Wayne",
|
||||
corporation_id=2002,
|
||||
corporation_name="Wayne Food",
|
||||
corporation_ticker="WYF",
|
||||
alliance_id=None
|
||||
)
|
||||
EveCharacter.objects.create(
|
||||
character_id=2,
|
||||
character_name='character.name2',
|
||||
corporation_id=9876,
|
||||
corporation_name='character.corp.name',
|
||||
corporation_ticker='c.c.t', # max 5 chars
|
||||
alliance_id=3456,
|
||||
alliance_name='character.alliance.name',
|
||||
)
|
||||
EveCharacter.objects.create(
|
||||
character_id=3,
|
||||
character_name='character.name3',
|
||||
corporation_id=9876,
|
||||
corporation_name='character.corp.name',
|
||||
corporation_ticker='c.c.t', # max 5 chars
|
||||
alliance_id=3456,
|
||||
alliance_name='character.alliance.name',
|
||||
)
|
||||
EveCharacter.objects.create(
|
||||
character_id=4,
|
||||
character_name='character.name4',
|
||||
corporation_id=9876,
|
||||
corporation_name='character.corp.name',
|
||||
corporation_ticker='c.c.t', # max 5 chars
|
||||
alliance_id=3456,
|
||||
alliance_name='character.alliance.name',
|
||||
)
|
||||
"""
|
||||
EveCharacter.objects.create(
|
||||
character_id=5,
|
||||
character_name='character.name5',
|
||||
corporation_id=9876,
|
||||
corporation_name='character.corp.name',
|
||||
corporation_ticker='c.c.t', # max 5 chars
|
||||
alliance_id=3456,
|
||||
alliance_name='character.alliance.name',
|
||||
)
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.affiliations = [
|
||||
{'character_id': 1, 'corporation_id': 5},
|
||||
{'character_id': 2, 'corporation_id': 9876, 'alliance_id': 3456},
|
||||
{'character_id': 3, 'corporation_id': 9876, 'alliance_id': 7456},
|
||||
{'character_id': 4, 'corporation_id': 9876, 'alliance_id': 3456}
|
||||
]
|
||||
self.names = [
|
||||
{'id': 1, 'name': 'character.name1'},
|
||||
{'id': 2, 'name': 'character.name2'},
|
||||
{'id': 3, 'name': 'character.name3'},
|
||||
{'id': 4, 'name': 'character.name4_new'}
|
||||
]
|
||||
|
||||
def test_normal_run(
|
||||
self,
|
||||
mock_provider,
|
||||
mock_update_corp,
|
||||
mock_update_alliance,
|
||||
mock_update_character,
|
||||
):
|
||||
def get_affiliations(characters: list):
|
||||
response = [x for x in self.affiliations if x['character_id'] in characters]
|
||||
mock_operator = Mock(**{'result.return_value': response})
|
||||
return mock_operator
|
||||
|
||||
def get_names(ids: list):
|
||||
response = [x for x in self.names if x['id'] in ids]
|
||||
mock_operator = Mock(**{'result.return_value': response})
|
||||
return mock_operator
|
||||
|
||||
mock_provider.client.Character.post_characters_affiliation.side_effect \
|
||||
= get_affiliations
|
||||
|
||||
mock_provider.client.Universe.post_universe_names.side_effect = get_names
|
||||
|
||||
# when
|
||||
run_model_update()
|
||||
|
||||
# then
|
||||
character_1001.refresh_from_db()
|
||||
self.assertEqual(
|
||||
mock_provider.client.Character.post_characters_affiliation.call_count, 2
|
||||
character_1001.corporation_id, 2001 # char has new corp
|
||||
)
|
||||
corporation_2003.refresh_from_db()
|
||||
self.assertEqual(
|
||||
mock_provider.client.Universe.post_universe_names.call_count, 2
|
||||
corporation_2003.alliance.alliance_id, 3001 # corp has new alliance
|
||||
)
|
||||
alliance_3001.refresh_from_db()
|
||||
self.assertEqual(
|
||||
alliance_3001.executor_corp_id, 2001 # alliance has been updated
|
||||
)
|
||||
|
||||
# character 1 has changed corp
|
||||
# character 2 no change
|
||||
# character 3 has changed alliance
|
||||
# character 4 has changed name
|
||||
self.assertEqual(mock_update_corp.apply_async.call_count, 1)
|
||||
self.assertEqual(
|
||||
int(mock_update_corp.apply_async.call_args[1]['args'][0]), 2345
|
||||
)
|
||||
self.assertEqual(mock_update_alliance.apply_async.call_count, 1)
|
||||
self.assertEqual(
|
||||
int(mock_update_alliance.apply_async.call_args[1]['args'][0]), 3456
|
||||
)
|
||||
characters_updated = {
|
||||
x[1]['args'][0] for x in mock_update_character.apply_async.call_args_list
|
||||
|
||||
@override_settings(CELERY_ALWAYS_EAGER=True)
|
||||
@patch('allianceauth.eveonline.tasks.update_character', wraps=update_character)
|
||||
@patch('allianceauth.eveonline.providers.esi_client_factory')
|
||||
@patch('allianceauth.eveonline.tasks.providers')
|
||||
@patch('allianceauth.eveonline.tasks.CHUNK_SIZE', 2)
|
||||
class TestUpdateCharacterChunk(TestCase):
|
||||
@staticmethod
|
||||
def _updated_character_ids(spy_update_character) -> set:
|
||||
"""Character IDs passed to update_character task for update."""
|
||||
return {
|
||||
x[1]["args"][0] for x in spy_update_character.apply_async.call_args_list
|
||||
}
|
||||
excepted = {1, 3, 4}
|
||||
self.assertSetEqual(characters_updated, excepted)
|
||||
|
||||
def test_ignore_character_not_in_affiliations(
|
||||
self,
|
||||
mock_provider,
|
||||
mock_update_corp,
|
||||
mock_update_alliance,
|
||||
mock_update_character,
|
||||
def test_should_update_corp_change(
|
||||
self, mock_providers, mock_esi_client_factory, spy_update_character
|
||||
):
|
||||
def get_affiliations(characters: list):
|
||||
response = [x for x in self.affiliations if x['character_id'] in characters]
|
||||
mock_operator = Mock(**{'result.return_value': response})
|
||||
return mock_operator
|
||||
# given
|
||||
mock_providers.provider.client = EsiClientStub()
|
||||
mock_esi_client_factory.return_value = EsiClientStub()
|
||||
character_1001 = EveCharacter.objects.create(
|
||||
character_id=1001,
|
||||
character_name="Bruce Wayne",
|
||||
corporation_id=2003,
|
||||
corporation_name="Wayne Energy",
|
||||
corporation_ticker="WEG",
|
||||
alliance_id=3001,
|
||||
alliance_name="Wayne Enterprises",
|
||||
alliance_ticker="WYE",
|
||||
)
|
||||
character_1002 = EveCharacter.objects.create(
|
||||
character_id=1002,
|
||||
character_name="Peter Parker",
|
||||
corporation_id=2001,
|
||||
corporation_name="Wayne Technologies",
|
||||
corporation_ticker="WTE",
|
||||
alliance_id=3001,
|
||||
alliance_name="Wayne Enterprises",
|
||||
alliance_ticker="WYE",
|
||||
)
|
||||
# when
|
||||
update_character_chunk([
|
||||
character_1001.character_id, character_1002.character_id
|
||||
])
|
||||
# then
|
||||
character_1001.refresh_from_db()
|
||||
self.assertEqual(character_1001.corporation_id, 2001)
|
||||
self.assertSetEqual(self._updated_character_ids(spy_update_character), {1001})
|
||||
|
||||
def get_names(ids: list):
|
||||
response = [x for x in self.names if x['id'] in ids]
|
||||
mock_operator = Mock(**{'result.return_value': response})
|
||||
return mock_operator
|
||||
|
||||
del self.affiliations[0]
|
||||
|
||||
mock_provider.client.Character.post_characters_affiliation.side_effect \
|
||||
= get_affiliations
|
||||
|
||||
mock_provider.client.Universe.post_universe_names.side_effect = get_names
|
||||
|
||||
run_model_update()
|
||||
characters_updated = {
|
||||
x[1]['args'][0] for x in mock_update_character.apply_async.call_args_list
|
||||
}
|
||||
excepted = {3, 4}
|
||||
self.assertSetEqual(characters_updated, excepted)
|
||||
|
||||
def test_ignore_character_not_in_names(
|
||||
self,
|
||||
mock_provider,
|
||||
mock_update_corp,
|
||||
mock_update_alliance,
|
||||
mock_update_character,
|
||||
def test_should_update_name_change(
|
||||
self, mock_providers, mock_esi_client_factory, spy_update_character
|
||||
):
|
||||
def get_affiliations(characters: list):
|
||||
response = [x for x in self.affiliations if x['character_id'] in characters]
|
||||
mock_operator = Mock(**{'result.return_value': response})
|
||||
return mock_operator
|
||||
# given
|
||||
mock_providers.provider.client = EsiClientStub()
|
||||
mock_esi_client_factory.return_value = EsiClientStub()
|
||||
character_1001 = EveCharacter.objects.create(
|
||||
character_id=1001,
|
||||
character_name="Batman",
|
||||
corporation_id=2001,
|
||||
corporation_name="Wayne Technologies",
|
||||
corporation_ticker="WTE",
|
||||
alliance_id=3001,
|
||||
alliance_name="Wayne Technologies",
|
||||
alliance_ticker="WYT",
|
||||
)
|
||||
# when
|
||||
update_character_chunk([character_1001.character_id])
|
||||
# then
|
||||
character_1001.refresh_from_db()
|
||||
self.assertEqual(character_1001.character_name, "Bruce Wayne")
|
||||
self.assertSetEqual(self._updated_character_ids(spy_update_character), {1001})
|
||||
|
||||
def get_names(ids: list):
|
||||
response = [x for x in self.names if x['id'] in ids]
|
||||
mock_operator = Mock(**{'result.return_value': response})
|
||||
return mock_operator
|
||||
def test_should_update_alliance_change(
|
||||
self, mock_providers, mock_esi_client_factory, spy_update_character
|
||||
):
|
||||
# given
|
||||
mock_providers.provider.client = EsiClientStub()
|
||||
mock_esi_client_factory.return_value = EsiClientStub()
|
||||
character_1001 = EveCharacter.objects.create(
|
||||
character_id=1001,
|
||||
character_name="Bruce Wayne",
|
||||
corporation_id=2001,
|
||||
corporation_name="Wayne Technologies",
|
||||
corporation_ticker="WTE",
|
||||
alliance_id=None,
|
||||
)
|
||||
# when
|
||||
update_character_chunk([character_1001.character_id])
|
||||
# then
|
||||
character_1001.refresh_from_db()
|
||||
self.assertEqual(character_1001.alliance_id, 3001)
|
||||
self.assertSetEqual(self._updated_character_ids(spy_update_character), {1001})
|
||||
|
||||
del self.names[3]
|
||||
def test_should_not_update_when_not_changed(
|
||||
self, mock_providers, mock_esi_client_factory, spy_update_character
|
||||
):
|
||||
# given
|
||||
mock_providers.provider.client = EsiClientStub()
|
||||
mock_esi_client_factory.return_value = EsiClientStub()
|
||||
character_1001 = EveCharacter.objects.create(
|
||||
character_id=1001,
|
||||
character_name="Bruce Wayne",
|
||||
corporation_id=2001,
|
||||
corporation_name="Wayne Technologies",
|
||||
corporation_ticker="WTE",
|
||||
alliance_id=3001,
|
||||
alliance_name="Wayne Technologies",
|
||||
alliance_ticker="WYT",
|
||||
)
|
||||
# when
|
||||
update_character_chunk([character_1001.character_id])
|
||||
# then
|
||||
self.assertSetEqual(self._updated_character_ids(spy_update_character), set())
|
||||
|
||||
mock_provider.client.Character.post_characters_affiliation.side_effect \
|
||||
= get_affiliations
|
||||
|
||||
mock_provider.client.Universe.post_universe_names.side_effect = get_names
|
||||
|
||||
run_model_update()
|
||||
characters_updated = {
|
||||
x[1]['args'][0] for x in mock_update_character.apply_async.call_args_list
|
||||
}
|
||||
excepted = {1, 3}
|
||||
self.assertSetEqual(characters_updated, excepted)
|
||||
def test_should_fall_back_to_single_updates_when_bulk_update_failed(
|
||||
self, mock_providers, mock_esi_client_factory, spy_update_character
|
||||
):
|
||||
# given
|
||||
mock_providers.provider.client.Character.post_characters_affiliation\
|
||||
.side_effect = OSError
|
||||
mock_esi_client_factory.return_value = EsiClientStub()
|
||||
character_1001 = EveCharacter.objects.create(
|
||||
character_id=1001,
|
||||
character_name="Bruce Wayne",
|
||||
corporation_id=2001,
|
||||
corporation_name="Wayne Technologies",
|
||||
corporation_ticker="WTE",
|
||||
alliance_id=3001,
|
||||
alliance_name="Wayne Technologies",
|
||||
alliance_ticker="WYT",
|
||||
)
|
||||
# when
|
||||
update_character_chunk([character_1001.character_id])
|
||||
# then
|
||||
self.assertSetEqual(self._updated_character_ids(spy_update_character), {1001})
|
||||
|
||||
@@ -127,6 +127,8 @@
|
||||
],
|
||||
bootstrap: true
|
||||
},
|
||||
"stateSave": true,
|
||||
"stateDuration": 0
|
||||
});
|
||||
});
|
||||
{% endblock %}
|
||||
|
||||
@@ -104,7 +104,9 @@
|
||||
"sortable": false,
|
||||
"targets": [2]
|
||||
},
|
||||
]
|
||||
],
|
||||
"stateSave": true,
|
||||
"stateDuration": 0
|
||||
});
|
||||
});
|
||||
{% endblock %}
|
||||
|
||||
@@ -29,15 +29,18 @@
|
||||
{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a data-toggle="tab" href="#leave">
|
||||
{% translate "Leave Requests" %}
|
||||
|
||||
{% if leaverequests %}
|
||||
<span class="badge">{{ leaverequests|length }}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
{% if not auto_leave %}
|
||||
<li>
|
||||
<a data-toggle="tab" href="#leave">
|
||||
{% translate "Leave Requests" %}
|
||||
|
||||
{% if leaverequests %}
|
||||
<span class="badge">{{ leaverequests|length }}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
||||
<div class="panel panel-default panel-tabs-aa">
|
||||
@@ -100,61 +103,63 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div id="leave" class="tab-pane">
|
||||
{% if leaverequests %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-aa">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% translate "Character" %}</th>
|
||||
<th>{% translate "Organization" %}</th>
|
||||
<th>{% translate "Group" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{% for leaverequest in leaverequests %}
|
||||
{% if not auto_leave %}
|
||||
<div id="leave" class="tab-pane">
|
||||
{% if leaverequests %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-aa">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>
|
||||
<img src="{{ leaverequest.main_char|character_portrait_url:32 }}" class="img-circle" style="margin-right: 1rem;">
|
||||
{% if leaverequest.main_char %}
|
||||
<a href="{{ leaverequest.main_char|evewho_character_url }}" target="_blank">
|
||||
{{ leaverequest.main_char.character_name }}
|
||||
</a>
|
||||
{% else %}
|
||||
{{ leaverequest.user.username }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if leaverequest.main_char %}
|
||||
<a href="{{ leaverequest.main_char|dotlan_corporation_url }}" target="_blank">
|
||||
{{ leaverequest.main_char.corporation_name }}
|
||||
</a><br>
|
||||
{{ leaverequest.main_char.alliance_name|default_if_none:"" }}
|
||||
{% else %}
|
||||
{% translate "(unknown)" %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ leaverequest.group.name }}</td>
|
||||
<td class="text-right">
|
||||
<a href="{% url 'groupmanagement:leave_accept_request' leaverequest.id %}" class="btn btn-success">
|
||||
{% translate "Accept" %}
|
||||
</a>
|
||||
|
||||
<a href="{% url 'groupmanagement:leave_reject_request' leaverequest.id %}" class="btn btn-danger">
|
||||
{% translate "Reject" %}
|
||||
</a>
|
||||
</td>
|
||||
<th>{% translate "Character" %}</th>
|
||||
<th>{% translate "Organization" %}</th>
|
||||
<th>{% translate "Group" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-warning text-center">{% translate "No group leave requests." %}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{% for leaverequest in leaverequests %}
|
||||
<tr>
|
||||
<td>
|
||||
<img src="{{ leaverequest.main_char|character_portrait_url:32 }}" class="img-circle" style="margin-right: 1rem;">
|
||||
{% if leaverequest.main_char %}
|
||||
<a href="{{ leaverequest.main_char|evewho_character_url }}" target="_blank">
|
||||
{{ leaverequest.main_char.character_name }}
|
||||
</a>
|
||||
{% else %}
|
||||
{{ leaverequest.user.username }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if leaverequest.main_char %}
|
||||
<a href="{{ leaverequest.main_char|dotlan_corporation_url }}" target="_blank">
|
||||
{{ leaverequest.main_char.corporation_name }}
|
||||
</a><br>
|
||||
{{ leaverequest.main_char.alliance_name|default_if_none:"" }}
|
||||
{% else %}
|
||||
{% translate "(unknown)" %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ leaverequest.group.name }}</td>
|
||||
<td class="text-right">
|
||||
<a href="{% url 'groupmanagement:leave_accept_request' leaverequest.id %}" class="btn btn-success">
|
||||
{% translate "Accept" %}
|
||||
</a>
|
||||
|
||||
<a href="{% url 'groupmanagement:leave_reject_request' leaverequest.id %}" class="btn btn-danger">
|
||||
{% translate "Reject" %}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-warning text-center">{% translate "No group leave requests." %}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from django.test import RequestFactory, TestCase
|
||||
from django.test import RequestFactory, TestCase, override_settings
|
||||
from django.urls import reverse
|
||||
|
||||
from allianceauth.tests.auth_utils import AuthUtils
|
||||
@@ -6,14 +6,80 @@ from allianceauth.tests.auth_utils import AuthUtils
|
||||
from .. import views
|
||||
|
||||
|
||||
def response_content_to_str(response) -> str:
|
||||
return response.content.decode(response.charset)
|
||||
|
||||
|
||||
class TestViews(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.factory = RequestFactory()
|
||||
self.user = AuthUtils.create_user('Bruce Wayne')
|
||||
self.user = AuthUtils.create_user('Peter Parker')
|
||||
self.user_with_manage_permission = AuthUtils.create_user('Bruce Wayne')
|
||||
|
||||
# set permissions
|
||||
AuthUtils.add_permission_to_user_by_name(
|
||||
'auth.group_management', self.user_with_manage_permission
|
||||
)
|
||||
|
||||
def test_groups_view_can_load(self):
|
||||
request = self.factory.get(reverse('groupmanagement:groups'))
|
||||
request.user = self.user
|
||||
response = views.groups_view(request)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_management_view_can_load_for_user_with_permissions(self):
|
||||
"""
|
||||
Test if user with management permissions can access the management view
|
||||
:return:
|
||||
"""
|
||||
|
||||
request = self.factory.get(reverse('groupmanagement:management'))
|
||||
request.user = self.user_with_manage_permission
|
||||
response = views.group_management(request)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_management_view_doesnt_load_for_user_without_permissions(self):
|
||||
"""
|
||||
Test if user without management permissions can't access the management view
|
||||
:return:
|
||||
"""
|
||||
|
||||
request = self.factory.get(reverse('groupmanagement:management'))
|
||||
request.user = self.user
|
||||
response = views.group_management(request)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
@override_settings(GROUPMANAGEMENT_AUTO_LEAVE=False)
|
||||
def test_leave_requests_tab_visible(self):
|
||||
"""
|
||||
Test if the leave requests tab is visible when GROUPMANAGEMENT_AUTO_LEAVE = False
|
||||
:return:
|
||||
"""
|
||||
|
||||
request = self.factory.get(reverse('groupmanagement:management'))
|
||||
request.user = self.user_with_manage_permission
|
||||
response = views.group_management(request)
|
||||
|
||||
content = response_content_to_str(response)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn('<a data-toggle="tab" href="#leave">', content)
|
||||
self.assertIn('<div id="leave" class="tab-pane">', content)
|
||||
|
||||
@override_settings(GROUPMANAGEMENT_AUTO_LEAVE=True)
|
||||
def test_leave_requests_tab_hidden(self):
|
||||
"""
|
||||
Test if the leave requests tab is hidden when GROUPMANAGEMENT_AUTO_LEAVE = True
|
||||
:return:
|
||||
"""
|
||||
|
||||
request = self.factory.get(reverse('groupmanagement:management'))
|
||||
request.user = self.user_with_manage_permission
|
||||
response = views.group_management(request)
|
||||
|
||||
content = response_content_to_str(response)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertNotIn('<a data-toggle="tab" href="#leave">', content)
|
||||
self.assertNotIn('<div id="leave" class="tab-pane">', content)
|
||||
|
||||
@@ -45,7 +45,11 @@ def group_management(request):
|
||||
logger.debug("Providing user {} with {} acceptrequests and {} leaverequests.".format(
|
||||
request.user, len(acceptrequests), len(leaverequests)))
|
||||
|
||||
render_items = {'acceptrequests': acceptrequests, 'leaverequests': leaverequests}
|
||||
render_items = {
|
||||
'acceptrequests': acceptrequests,
|
||||
'leaverequests': leaverequests,
|
||||
'auto_leave': getattr(settings, 'GROUPMANAGEMENT_AUTO_LEAVE', False),
|
||||
}
|
||||
|
||||
return render(request, 'groupmanagement/index.html', context=render_items)
|
||||
|
||||
|
||||
@@ -49,19 +49,22 @@ class NotificationManager(models.Manager):
|
||||
logger.info("Created notification %s", obj)
|
||||
return obj
|
||||
|
||||
def _max_notifications_per_user(self):
|
||||
"""return the maximum number of notifications allowed per user"""
|
||||
max_notifications = getattr(settings, 'NOTIFICATIONS_MAX_PER_USER', None)
|
||||
if (
|
||||
max_notifications is None
|
||||
or not isinstance(max_notifications, int)
|
||||
or max_notifications < 0
|
||||
):
|
||||
def _max_notifications_per_user(self) -> int:
|
||||
"""Maximum number of notifications allowed per user."""
|
||||
max_notifications = getattr(
|
||||
settings,
|
||||
"NOTIFICATIONS_MAX_PER_USER",
|
||||
self.model.NOTIFICATIONS_MAX_PER_USER_DEFAULT
|
||||
)
|
||||
try:
|
||||
max_notifications = int(max_notifications)
|
||||
except ValueError:
|
||||
max_notifications = None
|
||||
if max_notifications is None or max_notifications < 0:
|
||||
logger.warning(
|
||||
'NOTIFICATIONS_MAX_PER_USER setting is invalid. Using default.'
|
||||
"NOTIFICATIONS_MAX_PER_USER setting is invalid. Using default."
|
||||
)
|
||||
max_notifications = self.model.NOTIFICATIONS_MAX_PER_USER_DEFAULT
|
||||
|
||||
return max_notifications
|
||||
|
||||
def user_unread_count(self, user_pk: int) -> int:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import TestCase, override_settings
|
||||
|
||||
@@ -113,29 +114,53 @@ class TestUserNotify(TestCase):
|
||||
self.assertSetEqual(result, expected)
|
||||
|
||||
|
||||
@patch("allianceauth.notifications.managers.logger")
|
||||
@patch(
|
||||
MODULE_PATH + '.Notification.NOTIFICATIONS_MAX_PER_USER_DEFAULT',
|
||||
MODULE_PATH + ".Notification.NOTIFICATIONS_MAX_PER_USER_DEFAULT",
|
||||
NOTIFICATIONS_MAX_PER_USER_DEFAULT
|
||||
)
|
||||
class TestMaxNotificationsPerUser(TestCase):
|
||||
|
||||
@override_settings(NOTIFICATIONS_MAX_PER_USER=None)
|
||||
def test_reset_to_default_if_not_defined(self):
|
||||
@override_settings(NOTIFICATIONS_MAX_PER_USER=42)
|
||||
def test_should_use_custom_integer_setting(self, mock_logger):
|
||||
# when
|
||||
result = Notification.objects._max_notifications_per_user()
|
||||
expected = NOTIFICATIONS_MAX_PER_USER_DEFAULT
|
||||
self.assertEqual(result, expected)
|
||||
# then
|
||||
self.assertEqual(result, 42)
|
||||
self.assertFalse(mock_logger.warning.called)
|
||||
|
||||
@override_settings(NOTIFICATIONS_MAX_PER_USER='11')
|
||||
def test_reset_to_default_if_not_int(self):
|
||||
@override_settings(NOTIFICATIONS_MAX_PER_USER="42")
|
||||
def test_should_use_custom_string_setting(self, mock_logger):
|
||||
# when
|
||||
result = Notification.objects._max_notifications_per_user()
|
||||
expected = NOTIFICATIONS_MAX_PER_USER_DEFAULT
|
||||
self.assertEqual(result, expected)
|
||||
# then
|
||||
self.assertEqual(result, 42)
|
||||
self.assertFalse(mock_logger.warning.called)
|
||||
|
||||
@override_settings()
|
||||
def test_should_use_default_if_not_defined(self, mock_logger):
|
||||
# given
|
||||
del settings.NOTIFICATIONS_MAX_PER_USER
|
||||
# when
|
||||
result = Notification.objects._max_notifications_per_user()
|
||||
# then
|
||||
self.assertEqual(result, NOTIFICATIONS_MAX_PER_USER_DEFAULT)
|
||||
self.assertFalse(mock_logger.warning.called)
|
||||
|
||||
@override_settings(NOTIFICATIONS_MAX_PER_USER="abc")
|
||||
def test_should_reset_to_default_if_not_int(self, mock_logger):
|
||||
# when
|
||||
result = Notification.objects._max_notifications_per_user()
|
||||
# then
|
||||
self.assertEqual(result, NOTIFICATIONS_MAX_PER_USER_DEFAULT)
|
||||
self.assertTrue(mock_logger.warning.called)
|
||||
|
||||
@override_settings(NOTIFICATIONS_MAX_PER_USER=-1)
|
||||
def test_reset_to_default_if_lt_zero(self):
|
||||
def test_should_reset_to_default_if_lt_zero(self, mock_logger):
|
||||
# when
|
||||
result = Notification.objects._max_notifications_per_user()
|
||||
expected = NOTIFICATIONS_MAX_PER_USER_DEFAULT
|
||||
self.assertEqual(result, expected)
|
||||
# then
|
||||
self.assertEqual(result, NOTIFICATIONS_MAX_PER_USER_DEFAULT)
|
||||
self.assertTrue(mock_logger.warning.called)
|
||||
|
||||
|
||||
@patch('allianceauth.notifications.managers.cache')
|
||||
|
||||
@@ -73,6 +73,8 @@
|
||||
],
|
||||
bootstrap: true
|
||||
},
|
||||
"stateSave": true,
|
||||
"stateDuration": 0,
|
||||
drawCallback: function ( settings ) {
|
||||
let api = this.api();
|
||||
let rows = api.rows( {page:'current'} ).nodes();
|
||||
|
||||
@@ -106,8 +106,10 @@
|
||||
idx: 1
|
||||
}
|
||||
],
|
||||
bootstrap: true
|
||||
bootstrap: true,
|
||||
},
|
||||
"stateSave": true,
|
||||
"stateDuration": 0,
|
||||
drawCallback: function ( settings ) {
|
||||
let api = this.api();
|
||||
let rows = api.rows( {page:'current'} ).nodes();
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import AuthTS, Teamspeak3User, StateGroup
|
||||
from django.contrib.auth.models import Group
|
||||
from .models import AuthTS, Teamspeak3User, StateGroup, TSgroup
|
||||
from ...admin import ServicesUserAdmin
|
||||
from allianceauth.groupmanagement.models import ReservedGroupName
|
||||
|
||||
|
||||
@admin.register(Teamspeak3User)
|
||||
@@ -25,6 +26,16 @@ class AuthTSgroupAdmin(admin.ModelAdmin):
|
||||
fields = ('auth_group', 'ts_group')
|
||||
filter_horizontal = ('ts_group',)
|
||||
|
||||
def formfield_for_foreignkey(self, db_field, request, **kwargs):
|
||||
if db_field.name == 'auth_group':
|
||||
kwargs['queryset'] = Group.objects.exclude(name__in=ReservedGroupName.objects.values_list('name', flat=True))
|
||||
return super().formfield_for_foreignkey(db_field, request, **kwargs)
|
||||
|
||||
def formfield_for_manytomany(self, db_field, request, **kwargs):
|
||||
if db_field.name == 'ts_group':
|
||||
kwargs['queryset'] = TSgroup.objects.exclude(ts_group_name__in=ReservedGroupName.objects.values_list('name', flat=True))
|
||||
return super().formfield_for_manytomany(db_field, request, **kwargs)
|
||||
|
||||
def _ts_group(self, obj):
|
||||
return [x for x in obj.ts_group.all().order_by('ts_group_id')]
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ from django.conf import settings
|
||||
|
||||
from .util.ts3 import TS3Server, TeamspeakError
|
||||
from .models import TSgroup
|
||||
from allianceauth.groupmanagement.models import ReservedGroupName
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -156,32 +157,25 @@ class Teamspeak3Manager:
|
||||
logger.info(f"Removed user id {uid} from group id {groupid} on TS3 server.")
|
||||
|
||||
def _sync_ts_group_db(self):
|
||||
logger.debug("_sync_ts_group_db function called.")
|
||||
try:
|
||||
remote_groups = self._group_list()
|
||||
local_groups = TSgroup.objects.all()
|
||||
logger.debug("Comparing remote groups to TSgroup objects: %s" % local_groups)
|
||||
for key in remote_groups:
|
||||
logger.debug(f"Typecasting remote_group value at position {key} to int: {remote_groups[key]}")
|
||||
remote_groups[key] = int(remote_groups[key])
|
||||
managed_groups = {g:int(remote_groups[g]) for g in remote_groups if g in set(remote_groups.keys()) - set(ReservedGroupName.objects.values_list('name', flat=True))}
|
||||
remove = TSgroup.objects.exclude(ts_group_id__in=managed_groups.values())
|
||||
|
||||
if remove:
|
||||
logger.debug(f"Deleting {remove.count()} TSgroup models: not found on server, or reserved name.")
|
||||
remove.delete()
|
||||
|
||||
add = {g:managed_groups[g] for g in managed_groups if managed_groups[g] in set(managed_groups.values()) - set(TSgroup.objects.values_list("ts_group_id", flat=True))}
|
||||
if add:
|
||||
logger.debug(f"Adding {len(add)} new TSgroup models.")
|
||||
models = [TSgroup(ts_group_name=name, ts_group_id=add[name]) for name in add]
|
||||
TSgroup.objects.bulk_create(models)
|
||||
|
||||
for group in local_groups:
|
||||
logger.debug("Checking local group %s" % group)
|
||||
if group.ts_group_id not in remote_groups.values():
|
||||
logger.debug(
|
||||
f"Local group id {group.ts_group_id} not found on server. Deleting model {group}")
|
||||
TSgroup.objects.filter(ts_group_id=group.ts_group_id).delete()
|
||||
for key in remote_groups:
|
||||
g = TSgroup(ts_group_id=remote_groups[key], ts_group_name=key)
|
||||
q = TSgroup.objects.filter(ts_group_id=g.ts_group_id)
|
||||
if not q:
|
||||
logger.debug("Local group does not exist for TS group {}. Creating TSgroup model {}".format(
|
||||
remote_groups[key], g))
|
||||
g.save()
|
||||
except TeamspeakError as e:
|
||||
logger.error("Error occured while syncing TS group db: %s" % str(e))
|
||||
except:
|
||||
logger.exception("An unhandled exception has occured while syncing TS groups.")
|
||||
logger.error(f"Error occurred while syncing TS group db: {str(e)}")
|
||||
except Exception:
|
||||
logger.exception(f"An unhandled exception has occurred while syncing TS groups.")
|
||||
|
||||
def add_user(self, user, fmt_name):
|
||||
username_clean = self.__santatize_username(fmt_name[:30])
|
||||
@@ -240,7 +234,7 @@ class Teamspeak3Manager:
|
||||
logger.exception(f"Failed to delete user id {uid} from TS3 - received response {ret}")
|
||||
return False
|
||||
else:
|
||||
logger.warn("User with id %s not found on TS3 server. Assuming succesful deletion." % uid)
|
||||
logger.warning("User with id %s not found on TS3 server. Assuming succesful deletion." % uid)
|
||||
return True
|
||||
|
||||
def check_user_exists(self, uid):
|
||||
@@ -270,7 +264,8 @@ class Teamspeak3Manager:
|
||||
addgroups.append(ts_groups[ts_group_key])
|
||||
for user_ts_group_key in user_ts_groups:
|
||||
if user_ts_groups[user_ts_group_key] not in ts_groups.values():
|
||||
remgroups.append(user_ts_groups[user_ts_group_key])
|
||||
if not ReservedGroupName.objects.filter(name=user_ts_group_key).exists():
|
||||
remgroups.append(user_ts_groups[user_ts_group_key])
|
||||
|
||||
for g in addgroups:
|
||||
logger.info(f"Adding Teamspeak user {userid} into group {g}")
|
||||
|
||||
@@ -5,16 +5,18 @@ from django import urls
|
||||
from django.contrib.auth.models import User, Group, Permission
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db.models import signals
|
||||
from django.contrib.admin import AdminSite
|
||||
|
||||
from allianceauth.tests.auth_utils import AuthUtils
|
||||
from .auth_hooks import Teamspeak3Service
|
||||
from .models import Teamspeak3User, AuthTS, TSgroup, StateGroup
|
||||
from .tasks import Teamspeak3Tasks
|
||||
from .signals import m2m_changed_authts_group, post_save_authts, post_delete_authts
|
||||
from .admin import AuthTSgroupAdmin
|
||||
|
||||
from .manager import Teamspeak3Manager
|
||||
from .util.ts3 import TeamspeakError
|
||||
from allianceauth.authentication.models import State
|
||||
from allianceauth.groupmanagement.models import ReservedGroupName
|
||||
|
||||
MODULE_PATH = 'allianceauth.services.modules.teamspeak3'
|
||||
DEFAULT_AUTH_GROUP = 'Member'
|
||||
@@ -315,6 +317,9 @@ class Teamspeak3SignalsTestCase(TestCase):
|
||||
|
||||
|
||||
class Teamspeak3ManagerTestCase(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.reserved = ReservedGroupName.objects.create(name='reserved', reason='tests', created_by='Bob, praise be!')
|
||||
|
||||
@staticmethod
|
||||
def my_side_effect(*args, **kwargs):
|
||||
@@ -334,8 +339,135 @@ class Teamspeak3ManagerTestCase(TestCase):
|
||||
manager._server = server
|
||||
|
||||
# create test data
|
||||
user = User.objects.create_user("dummy")
|
||||
user.profile.state = State.objects.filter(name="Member").first()
|
||||
user = AuthUtils.create_user("dummy")
|
||||
AuthUtils.assign_state(user, AuthUtils.get_member_state())
|
||||
|
||||
# perform test
|
||||
manager.add_user(user, "Dummy User")
|
||||
|
||||
@mock.patch.object(Teamspeak3Manager, '_get_userid')
|
||||
@mock.patch.object(Teamspeak3Manager, '_user_group_list')
|
||||
@mock.patch.object(Teamspeak3Manager, '_add_user_to_group')
|
||||
@mock.patch.object(Teamspeak3Manager, '_remove_user_from_group')
|
||||
def test_update_groups_add(self, remove, add, groups, userid):
|
||||
"""Add to one group"""
|
||||
userid.return_value = 1
|
||||
groups.return_value = {'test': 1}
|
||||
|
||||
Teamspeak3Manager().update_groups(1, {'test': 1, 'dummy': 2})
|
||||
self.assertEqual(add.call_count, 1)
|
||||
self.assertEqual(remove.call_count, 0)
|
||||
self.assertEqual(add.call_args[0][1], 2)
|
||||
|
||||
@mock.patch.object(Teamspeak3Manager, '_get_userid')
|
||||
@mock.patch.object(Teamspeak3Manager, '_user_group_list')
|
||||
@mock.patch.object(Teamspeak3Manager, '_add_user_to_group')
|
||||
@mock.patch.object(Teamspeak3Manager, '_remove_user_from_group')
|
||||
def test_update_groups_remove(self, remove, add, groups, userid):
|
||||
"""Remove from one group"""
|
||||
userid.return_value = 1
|
||||
groups.return_value = {'test': '1', 'dummy': '2'}
|
||||
|
||||
Teamspeak3Manager().update_groups(1, {'test': 1})
|
||||
self.assertEqual(add.call_count, 0)
|
||||
self.assertEqual(remove.call_count, 1)
|
||||
self.assertEqual(remove.call_args[0][1], 2)
|
||||
|
||||
@mock.patch.object(Teamspeak3Manager, '_get_userid')
|
||||
@mock.patch.object(Teamspeak3Manager, '_user_group_list')
|
||||
@mock.patch.object(Teamspeak3Manager, '_add_user_to_group')
|
||||
@mock.patch.object(Teamspeak3Manager, '_remove_user_from_group')
|
||||
def test_update_groups_remove_reserved(self, remove, add, groups, userid):
|
||||
"""Remove from one group, but do not touch reserved group"""
|
||||
userid.return_value = 1
|
||||
groups.return_value = {'test': 1, 'dummy': 2, self.reserved.name: 3}
|
||||
|
||||
Teamspeak3Manager().update_groups(1, {'test': 1})
|
||||
self.assertEqual(add.call_count, 0)
|
||||
self.assertEqual(remove.call_count, 1)
|
||||
self.assertEqual(remove.call_args[0][1], 2)
|
||||
|
||||
@mock.patch.object(Teamspeak3Manager, '_group_list')
|
||||
def test_sync_group_db_create(self, group_list):
|
||||
"""Populate the list of all TSgroups"""
|
||||
group_list.return_value = {'allowed':'1', 'also allowed':'2'}
|
||||
Teamspeak3Manager()._sync_ts_group_db()
|
||||
self.assertEqual(TSgroup.objects.all().count(), 2)
|
||||
|
||||
@mock.patch.object(Teamspeak3Manager, '_group_list')
|
||||
def test_sync_group_db_delete(self, group_list):
|
||||
"""Populate the list of all TSgroups, and delete one which no longer exists"""
|
||||
TSgroup.objects.create(ts_group_name='deleted', ts_group_id=3)
|
||||
group_list.return_value = {'allowed': '1'}
|
||||
Teamspeak3Manager()._sync_ts_group_db()
|
||||
self.assertEqual(TSgroup.objects.all().count(), 1)
|
||||
self.assertFalse(TSgroup.objects.filter(ts_group_name='deleted').exists())
|
||||
|
||||
@mock.patch.object(Teamspeak3Manager, '_group_list')
|
||||
def test_sync_group_db_dont_create_reserved(self, group_list):
|
||||
"""Populate the list of all TSgroups, ignoring a reserved group name"""
|
||||
group_list.return_value = {'allowed': '1', 'reserved': '4'}
|
||||
Teamspeak3Manager()._sync_ts_group_db()
|
||||
self.assertEqual(TSgroup.objects.all().count(), 1)
|
||||
self.assertFalse(TSgroup.objects.filter(ts_group_name='reserved').exists())
|
||||
|
||||
@mock.patch.object(Teamspeak3Manager, '_group_list')
|
||||
def test_sync_group_db_delete_reserved(self, group_list):
|
||||
"""Populate the list of all TSgroups, deleting the TSgroup model for one which has become reserved"""
|
||||
TSgroup.objects.create(ts_group_name='reserved', ts_group_id=4)
|
||||
group_list.return_value = {'allowed': '1', 'reserved': '4'}
|
||||
Teamspeak3Manager()._sync_ts_group_db()
|
||||
self.assertEqual(TSgroup.objects.all().count(), 1)
|
||||
self.assertFalse(TSgroup.objects.filter(ts_group_name='reserved').exists())
|
||||
|
||||
@mock.patch.object(Teamspeak3Manager, '_group_list')
|
||||
def test_sync_group_db_partial_addition(self, group_list):
|
||||
"""Some TSgroups already exist in database, add new ones"""
|
||||
TSgroup.objects.create(ts_group_name='allowed', ts_group_id=1)
|
||||
group_list.return_value = {'allowed': '1', 'also allowed': '2'}
|
||||
Teamspeak3Manager()._sync_ts_group_db()
|
||||
self.assertEqual(TSgroup.objects.all().count(), 2)
|
||||
|
||||
@mock.patch.object(Teamspeak3Manager, '_group_list')
|
||||
def test_sync_group_db_partial_removal(self, group_list):
|
||||
"""One TSgroup has been deleted on server, so remove its model"""
|
||||
TSgroup.objects.create(ts_group_name='allowed', ts_group_id=1)
|
||||
TSgroup.objects.create(ts_group_name='also allowed', ts_group_id=2)
|
||||
group_list.return_value = {'allowed': '1'}
|
||||
Teamspeak3Manager()._sync_ts_group_db()
|
||||
self.assertEqual(TSgroup.objects.all().count(), 1)
|
||||
|
||||
|
||||
class MockRequest:
|
||||
pass
|
||||
|
||||
|
||||
class MockSuperUser:
|
||||
def has_perm(self, perm, obj=None):
|
||||
return True
|
||||
|
||||
|
||||
request = MockRequest()
|
||||
request.user = MockSuperUser()
|
||||
|
||||
|
||||
class Teamspeak3AdminTestCase(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.site = AdminSite()
|
||||
cls.admin = AuthTSgroupAdmin(AuthTS, cls.site)
|
||||
cls.group = Group.objects.create(name='test')
|
||||
cls.ts_group = TSgroup.objects.create(ts_group_name='test')
|
||||
|
||||
def test_field_queryset_no_reserved_names(self):
|
||||
"""Ensure all groups are listed when no reserved names"""
|
||||
form = self.admin.get_form(request)
|
||||
self.assertQuerysetEqual(form.base_fields['auth_group']._get_queryset(), Group.objects.all())
|
||||
self.assertQuerysetEqual(form.base_fields['ts_group']._get_queryset(), TSgroup.objects.all())
|
||||
|
||||
def test_field_queryset_reserved_names(self):
|
||||
"""Ensure reserved group names are filtered out"""
|
||||
ReservedGroupName.objects.bulk_create([ReservedGroupName(name='test', reason='tests', created_by='Bob')])
|
||||
form = self.admin.get_form(request)
|
||||
self.assertQuerysetEqual(form.base_fields['auth_group']._get_queryset(), Group.objects.none())
|
||||
self.assertQuerysetEqual(form.base_fields['ts_group']._get_queryset(), TSgroup.objects.none())
|
||||
|
||||
@@ -267,7 +267,9 @@ ESC to cancel{% endblocktrans %}"id="blah"></i></th>
|
||||
"targets": [4, 5],
|
||||
"type": "num"
|
||||
}
|
||||
]
|
||||
],
|
||||
"stateSave": true,
|
||||
"stateDuration": 0
|
||||
});
|
||||
|
||||
// tooltip
|
||||
|
||||
@@ -95,6 +95,11 @@ ul.list-group.list-group-horizontal > li.list-group-item {
|
||||
.table-aa > tbody > tr:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.task-status-progress-bar {
|
||||
font-size: 15px!important;
|
||||
line-height: normal!important;
|
||||
}
|
||||
}
|
||||
|
||||
/* highlight active menu items
|
||||
|
||||
@@ -1,58 +1,20 @@
|
||||
$(document).ready(function () {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* check time
|
||||
* @param i
|
||||
* @returns {string}
|
||||
*/
|
||||
let checkTime = function (i) {
|
||||
if (i < 10) {
|
||||
i = '0' + i;
|
||||
}
|
||||
|
||||
return i;
|
||||
};
|
||||
|
||||
/**
|
||||
* render a JS clock for Eve Time
|
||||
* @param element
|
||||
* @param utcOffset
|
||||
*/
|
||||
let renderClock = function (element, utcOffset) {
|
||||
let today = new Date();
|
||||
let h = today.getUTCHours();
|
||||
let m = today.getUTCMinutes();
|
||||
|
||||
h = h + utcOffset;
|
||||
|
||||
if (h > 24) {
|
||||
h = h - 24;
|
||||
}
|
||||
|
||||
if (h < 0) {
|
||||
h = h + 24;
|
||||
}
|
||||
|
||||
h = checkTime(h);
|
||||
m = checkTime(m);
|
||||
const renderClock = function (element) {
|
||||
const datetimeNow = new Date();
|
||||
const h = String(datetimeNow.getUTCHours()).padStart(2, '0');
|
||||
const m = String(datetimeNow.getUTCMinutes()).padStart(2, '0');
|
||||
|
||||
element.html(h + ':' + m);
|
||||
|
||||
setTimeout(function () {
|
||||
renderClock(element, 0);
|
||||
}, 500);
|
||||
};
|
||||
|
||||
/**
|
||||
* functions that need to be executed on load
|
||||
*/
|
||||
let init = function () {
|
||||
renderClock($('.eve-time-wrapper .eve-time-clock'), 0);
|
||||
};
|
||||
|
||||
/**
|
||||
* start the show
|
||||
*/
|
||||
init();
|
||||
// Start the Eve time clock in the top menu bar
|
||||
setInterval(function () {
|
||||
renderClock($('.eve-time-wrapper .eve-time-clock'));
|
||||
}, 500);
|
||||
});
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 6.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 4.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 4.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.3 KiB |
6
allianceauth/static/js/jquery-ui/1.12.1/css/jquery-ui.min.css
vendored
Normal file
6
allianceauth/static/js/jquery-ui/1.12.1/css/jquery-ui.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -0,0 +1,11 @@
|
||||
{% load humanize %}
|
||||
|
||||
<div
|
||||
class="progress-bar progress-bar-{{ level }} task-status-progress-bar"
|
||||
role="progressbar"
|
||||
aria-valuenow="{% widthratio tasks_count tasks_total 100 %}"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
style="width: {% widthratio tasks_count tasks_total 100 %}%;">
|
||||
{% widthratio tasks_count tasks_total 100 %}%
|
||||
</div>
|
||||
@@ -1,4 +1,6 @@
|
||||
{% load i18n %}
|
||||
{% load humanize %}
|
||||
|
||||
<div class="col-sm-12">
|
||||
<div class="row vertical-flexbox-row2">
|
||||
<div class="col-sm-6">
|
||||
@@ -75,29 +77,25 @@
|
||||
<div class="panel panel-primary" style="height:50%;">
|
||||
<div class="panel-heading text-center"><h3 class="panel-title">{% translate "Task Queue" %}</h3></div>
|
||||
<div class="panel-body flex-center-horizontal">
|
||||
<div class="progress" style="height: 21px;">
|
||||
<div class="progress-bar
|
||||
{% if task_queue_length > 500 %}
|
||||
progress-bar-danger
|
||||
{% elif task_queue_length > 100 %}
|
||||
progress-bar-warning
|
||||
{% else %}
|
||||
progress-bar-success
|
||||
{% endif %}
|
||||
" role="progressbar" aria-valuenow="{% widthratio task_queue_length 500 100 %}"
|
||||
aria-valuemin="0" aria-valuemax="100"
|
||||
style="width: {% widthratio task_queue_length 500 100 %}%;">
|
||||
</div>
|
||||
<p>
|
||||
{% blocktranslate with total=tasks_total|intcomma latest=earliest_task|timesince|default:"?" %}
|
||||
Status of {{ total }} processed tasks • last {{ latest }}
|
||||
{% endblocktranslate %}
|
||||
</p>
|
||||
<div
|
||||
class="progress"
|
||||
style="height: 21px;"
|
||||
title="{{ tasks_succeeded|intcomma }} succeeded, {{ tasks_retried|intcomma }} retried, {{ tasks_failed|intcomma }} failed"
|
||||
>
|
||||
{% include "allianceauth/admin-status/celery_bar_partial.html" with label="suceeded" level="success" tasks_count=tasks_succeeded %}
|
||||
{% include "allianceauth/admin-status/celery_bar_partial.html" with label="retried" level="info" tasks_count=tasks_retried %}
|
||||
{% include "allianceauth/admin-status/celery_bar_partial.html" with label="failed" level="danger" tasks_count=tasks_failed %}
|
||||
</div>
|
||||
{% if task_queue_length < 0 %}
|
||||
{% translate "Error retrieving task queue length" %}
|
||||
{% else %}
|
||||
{% blocktrans trimmed count tasks=task_queue_length %}
|
||||
{{ tasks }} task
|
||||
{% plural %}
|
||||
{{ tasks }} tasks
|
||||
{% endblocktrans %}
|
||||
{% endif %}
|
||||
<p>
|
||||
{% blocktranslate with queue_length=task_queue_length|default_if_none:"?"|intcomma %}
|
||||
{{ queue_length }} queued tasks
|
||||
{% endblocktranslate %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
<!-- Start jQuery UI CSS from cdnjs -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.css" integrity="sha512-aOG0c6nPNzGk+5zjwyJaoRUgCdOrfSDhmMID2u4+OIslr0GjpLKo7Xm0Ao3xmpM4T8AmIouRkqwj1nrdVsLKEQ==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
<!-- End jQuery UI CSS from cdnjs -->
|
||||
{% load static %}
|
||||
<!-- Start jQuery UI CSS from Alliance Auth -->
|
||||
<!-- CDNs all contain theme.css, which is not supposed to be in the base CSS, Which is why this is uniquely bundled in not using a CDN -->
|
||||
<link rel="stylesheet" href="{% static 'js/jquery-ui/1.12.1/css/jquery-ui.min.css' %}" integrity="sha512-7smZe1765O+Mm1UZH46SzaFClbRX7dEs01lB9lqU91oocmugWWfQXVQNVr5tEwktYSqwJMErEfr4GvflXMgTPA==" crossorigin="anonymous" referrerpolicy="no-referrer"/>
|
||||
<!-- End jQuery UI CSS from aa-gdpr -->
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
import requests
|
||||
import amqp.exceptions
|
||||
from packaging.version import Version as Pep440Version, InvalidVersion
|
||||
import requests
|
||||
from celery.app import app_or_default
|
||||
from packaging.version import InvalidVersion
|
||||
from packaging.version import Version as Pep440Version
|
||||
|
||||
from django import template
|
||||
from django.conf import settings
|
||||
@@ -11,6 +13,7 @@ from django.core.cache import cache
|
||||
|
||||
from allianceauth import __version__
|
||||
|
||||
from ..authentication.task_statistics.counters import dashboard_results
|
||||
|
||||
register = template.Library()
|
||||
|
||||
@@ -36,30 +39,51 @@ logger = logging.getLogger(__name__)
|
||||
@register.inclusion_tag('allianceauth/admin-status/overview.html')
|
||||
def status_overview() -> dict:
|
||||
response = {
|
||||
'notifications': list(),
|
||||
'current_version': __version__,
|
||||
'task_queue_length': -1,
|
||||
"notifications": list(),
|
||||
"current_version": __version__,
|
||||
"task_queue_length": None,
|
||||
"tasks_succeeded": 0,
|
||||
"tasks_retried": 0,
|
||||
"tasks_failed": 0,
|
||||
"tasks_total": 0,
|
||||
"tasks_hours": 0,
|
||||
"earliest_task": None
|
||||
}
|
||||
response.update(_current_notifications())
|
||||
response.update(_current_version_summary())
|
||||
response.update({'task_queue_length': _fetch_celery_queue_length()})
|
||||
response.update(_celery_stats())
|
||||
return response
|
||||
|
||||
|
||||
def _fetch_celery_queue_length() -> int:
|
||||
def _celery_stats() -> dict:
|
||||
hours = getattr(settings, "ALLIANCEAUTH_DASHBOARD_TASKS_MAX_HOURS", 24)
|
||||
results = dashboard_results(hours=hours)
|
||||
return {
|
||||
"tasks_succeeded": results.succeeded,
|
||||
"tasks_retried": results.retried,
|
||||
"tasks_failed": results.failed,
|
||||
"tasks_total": results.total,
|
||||
"tasks_hours": results.hours,
|
||||
"earliest_task": results.earliest_task
|
||||
}
|
||||
|
||||
|
||||
def _fetch_celery_queue_length() -> Optional[int]:
|
||||
try:
|
||||
app = app_or_default(None)
|
||||
with app.connection_or_acquire() as conn:
|
||||
return conn.default_channel.queue_declare(
|
||||
result = conn.default_channel.queue_declare(
|
||||
queue=getattr(settings, 'CELERY_DEFAULT_QUEUE', 'celery'),
|
||||
passive=True
|
||||
).message_count
|
||||
)
|
||||
return result.message_count
|
||||
except amqp.exceptions.ChannelError:
|
||||
# Queue doesn't exist, probably empty
|
||||
return 0
|
||||
except Exception:
|
||||
logger.exception("Failed to get celery queue length")
|
||||
return -1
|
||||
return None
|
||||
|
||||
|
||||
def _current_notifications() -> dict:
|
||||
|
||||
@@ -174,7 +174,7 @@ class AuthUtils:
|
||||
alliance_id=None,
|
||||
alliance_name='',
|
||||
disconnect_signals=False
|
||||
):
|
||||
) -> EveCharacter:
|
||||
"""new version that works in all cases"""
|
||||
if disconnect_signals:
|
||||
cls.disconnect_signals()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
PROTOCOL=https://
|
||||
AUTH_SUBDOMAIN=%AUTH_SUBDOMAIN%
|
||||
DOMAIN=%DOMAIN%
|
||||
AA_DOCKER_TAG=registry.gitlab.com/allianceauth/allianceauth/auth:v2.9
|
||||
AA_DOCKER_TAG=registry.gitlab.com/allianceauth/allianceauth/auth:v2.11
|
||||
|
||||
# Nginx Proxy Manager
|
||||
PROXY_HTTP_PORT=80
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
FROM python:3.9-slim
|
||||
ARG AUTH_VERSION=2.9.0
|
||||
ARG AUTH_VERSION=2.11.1
|
||||
ARG AUTH_PACKAGE=allianceauth==${AUTH_VERSION}
|
||||
ENV VIRTUAL_ENV=/opt/venv
|
||||
ENV AUTH_USER=allianceauth
|
||||
@@ -39,7 +39,6 @@ RUN allianceauth start myauth
|
||||
COPY /allianceauth/project_template/project_name/settings/local.py ${AUTH_HOME}/myauth/myauth/settings/local.py
|
||||
RUN allianceauth update myauth
|
||||
RUN mkdir -p ${STATIC_BASE}/myauth/static
|
||||
RUN python ${AUTH_HOME}/myauth/manage.py collectstatic --noinput
|
||||
COPY /docker/conf/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
||||
RUN echo 'alias auth="python $AUTH_HOME/myauth/manage.py"' >> ~/.bashrc && \
|
||||
echo 'alias supervisord="supervisord -c /etc/supervisor/conf.d/supervisord.conf"' >> ~/.bashrc && \
|
||||
|
||||
@@ -8,7 +8,7 @@ You should have the following available on the system you are using to set this
|
||||
|
||||
## Setup Guide
|
||||
|
||||
1. run `bash <(curl -s https://gitlab.com/allianceauth/allianceauth/-/raw/v2.9.x/docker/scripts/download.sh)`. This will download all the files you need to install auth and place them in a directory named `aa-docker`. Feel free to rename/move this folder.
|
||||
1. run `bash <(curl -s https://gitlab.com/allianceauth/allianceauth/-/raw/v2.11.x/docker/scripts/download.sh)`. This will download all the files you need to install auth and place them in a directory named `aa-docker`. Feel free to rename/move this folder.
|
||||
1. run `./scripts/prepare-env.sh` to set up your environment
|
||||
1. (optional) Change `PROTOCOL` to `http://` if not using SSL in `.env`
|
||||
1. run `docker-compose --env-file=.env up -d` (NOTE: if this command hangs, follow the instructions [here](https://www.digitalocean.com/community/tutorials/how-to-setup-additional-entropy-for-cloud-servers-using-haveged))
|
||||
@@ -67,3 +67,16 @@ _NOTE: If you specify a version of allianceauth in your `requirements.txt` in a
|
||||
1. Update the versions in your `requirements.txt` file
|
||||
1. Run `docker-compose build`
|
||||
1. Run `docker-compose --env-file=.env up -d`
|
||||
|
||||
## Notes
|
||||
|
||||
### Apple M1 Support
|
||||
If you want to run locally on an M1 powered Apple device, you'll need to add `platform: linux/x86_64` under each container in `docker-compose.yml` as the auth container is not compiled for ARM (other containers may work without this, but it's known to work if added to all containers).
|
||||
|
||||
Example:
|
||||
|
||||
```yaml
|
||||
redis:
|
||||
platform: linux/x86_64
|
||||
image: redis:6.2
|
||||
```
|
||||
|
||||
@@ -52,7 +52,7 @@ services:
|
||||
- auth_mysql
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana:8.2
|
||||
image: grafana/grafana-oss:8.3.2
|
||||
restart: always
|
||||
depends_on:
|
||||
- auth_mysql
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
git clone -b build-docker-image https://gitlab.com/allianceauth/allianceauth.git aa-git
|
||||
git clone https://gitlab.com/allianceauth/allianceauth.git aa-git
|
||||
cp -R aa-git/docker ./aa-docker
|
||||
rm -rf aa-git
|
||||
|
||||
@@ -42,6 +42,7 @@ from recommonmark.transform import AutoStructify
|
||||
extensions = [
|
||||
'sphinx_rtd_theme',
|
||||
'sphinx.ext.autodoc',
|
||||
'sphinx.ext.napoleon',
|
||||
'recommonmark',
|
||||
]
|
||||
|
||||
|
||||
@@ -10,6 +10,12 @@ To Opt-Out, modify our pre-loaded token using the Admin dashboard */admin/analyt
|
||||
|
||||
Each of the three features Daily Stats, Celery Events and Page Views can be enabled/Disabled independently.
|
||||
|
||||
Alternatively, you can fully opt out of analytics with the following optional setting:
|
||||
|
||||
```python
|
||||
ANALYTICS_DISABLED = True
|
||||
```
|
||||
|
||||

|
||||
|
||||
## What
|
||||
@@ -58,6 +64,8 @@ This data is stored in a Team Google Analytics Dashboard. The Maintainers all ha
|
||||
|
||||
### Analytics Event
|
||||
|
||||
```eval_rst
|
||||
.. automodule:: allianceauth.analytics.tasks
|
||||
:members: analytics_event
|
||||
:undoc-members:
|
||||
```
|
||||
|
||||
@@ -7,3 +7,18 @@ The content of the dashboard is specific to the logged in user. It has a sidebar
|
||||
For admin users the dashboard shows additional technical information about the AA instance.
|
||||
|
||||

|
||||
|
||||
## Settings
|
||||
|
||||
Here is a list of available settings for the dashboard. They can be configured by adding them to your AA settings file (``local.py``).
|
||||
Note that all settings are optional and the app will use the documented default settings if they are not used.
|
||||
|
||||
```eval_rst
|
||||
+-----------------------------------------------------+-------------------------------------------------------------------------+-----------+
|
||||
| Name | Description | Default |
|
||||
+=====================================================+=========================================================================+===========+
|
||||
| ``ALLIANCEAUTH_DASHBOARD_TASKS_MAX_HOURS`` | Statistics will be calculated for task events not older than max hours. | ``24`` |
|
||||
+-----------------------------------------------------+-------------------------------------------------------------------------+-----------+
|
||||
| ``ALLIANCEAUTH_DASHBOARD_TASK_STATISTICS_DISABLED`` | Disables recording of task statistics. Used mainly in development. | ``False`` |
|
||||
+-----------------------------------------------------+-------------------------------------------------------------------------+-----------+
|
||||
```
|
||||
|
||||
@@ -48,7 +48,7 @@ When using Alliance Auth to manage external services like Discord, Auth will aut
|
||||
|
||||
```eval_rst
|
||||
.. note::
|
||||
While this feature can help to avoid naming conflicts with groups on external services, the respective service component in Alliance Auth also needs to be build in such a way that it knows how to prevent these conflicts. Currently only the Discord service has this ability.
|
||||
While this feature can help to avoid naming conflicts with groups on external services, the respective service component in Alliance Auth also needs to be build in such a way that it knows how to prevent these conflicts. Currently only the Discord and Teamspeak3 services have this ability.
|
||||
```
|
||||
|
||||
## Managing groups
|
||||
@@ -90,11 +90,16 @@ This allows you to more finely control who has access to manage which groups.
|
||||
|
||||
### Auto Leave
|
||||
|
||||
By default in AA, Both requests and leaves for non-open groups must be approved by a group manager. If you wish to allow users to leave groups without requiring approvals, add the following lines to your `local.py`
|
||||
By default, in AA both requests and leaves for non-open groups must be approved by a group manager. If you wish to allow users to leave groups without requiring approvals, add the following lines to your `local.py`
|
||||
|
||||
```python
|
||||
## Allows users to freely leave groups without requiring approval.
|
||||
AUTO_LEAVE = True
|
||||
GROUPMANAGEMENT_AUTO_LEAVE = True
|
||||
```
|
||||
|
||||
```eval_rst
|
||||
.. note::
|
||||
Before you set `GROUPMANAGEMENT_AUTO_LEAVE = True`, make sure there are no pending leave requests, as this option will hide the "Leave Requests" tab.
|
||||
```
|
||||
|
||||
## Settings
|
||||
|
||||
@@ -160,7 +160,7 @@ This error generally means teamspeak returned an error message that went unhandl
|
||||
|
||||
This most commonly happens when your teamspeak server is externally hosted. You need to add the auth server IP to the teamspeak serverquery whitelist. This varies by provider.
|
||||
|
||||
If you have SSH access to the server hosting it, you need to locate the teamspeak server folder and add the auth server IP on a new line in `server_query_whitelist.txt`
|
||||
If you have SSH access to the server hosting it, you need to locate the teamspeak server folder and add the auth server IP on a new line in `query_ip_allowlist.txt` (named `query_ip_whitelist.txt` on older teamspeak versions).
|
||||
|
||||
### `520 invalid loginname or password`
|
||||
|
||||
|
||||
4
setup.py
4
setup.py
@@ -30,10 +30,10 @@ install_requires = [
|
||||
'django-celery-beat>=2.0.0',
|
||||
|
||||
'openfire-restapi',
|
||||
'sleekxmpp',
|
||||
'sleekxmpp<=1.3.2',
|
||||
'pydiscourse',
|
||||
|
||||
'django-esi>=3.0.0,<4.0.0'
|
||||
'django-esi>=4.0.1,<5.0.0'
|
||||
]
|
||||
|
||||
testing_extras = [
|
||||
|
||||
@@ -154,3 +154,5 @@ PASSWORD_HASHERS = [
|
||||
]
|
||||
|
||||
LOGGING = None # Comment out to enable logging for debugging
|
||||
|
||||
ALLIANCEAUTH_DASHBOARD_TASK_STATISTICS_DISABLED = True # disable for tests
|
||||
|
||||
@@ -27,3 +27,5 @@ PASSWORD_HASHERS = [
|
||||
]
|
||||
|
||||
LOGGING = None # Comment out to enable logging for debugging
|
||||
|
||||
ALLIANCEAUTH_DASHBOARD_TASK_STATISTICS_DISABLED = True # disable for tests
|
||||
|
||||
3
tox.ini
3
tox.ini
@@ -1,7 +1,7 @@
|
||||
[tox]
|
||||
skipsdist = true
|
||||
usedevelop = true
|
||||
envlist = py{37,38,39,310}-{all,core}
|
||||
envlist = py{37,38,39,310,311}-{all,core}
|
||||
|
||||
[testenv]
|
||||
setenv =
|
||||
@@ -12,6 +12,7 @@ basepython =
|
||||
py38: python3.8
|
||||
py39: python3.9
|
||||
py310: python3.10
|
||||
py311: python3.11
|
||||
deps=
|
||||
coverage
|
||||
install_command = pip install -e ".[testing]" -U {opts} {packages}
|
||||
|
||||
Reference in New Issue
Block a user