[v4.x] ESI Alerts Dashboard Widget

This commit is contained in:
Aaron Kable 2024-02-17 10:48:37 +00:00 committed by Ariel Rin
parent dfcbad3476
commit ebb40deb7f
6 changed files with 288 additions and 17 deletions

View File

@ -1,6 +1,6 @@
from allianceauth.hooks import DashboardItemHook from allianceauth.hooks import DashboardItemHook
from allianceauth import hooks from allianceauth import hooks
from .views import dashboard_characters, dashboard_groups, dashboard_admin from .views import dashboard_characters, dashboard_esi_check, dashboard_groups, dashboard_admin
class UserCharactersHook(DashboardItemHook): class UserCharactersHook(DashboardItemHook):
@ -26,6 +26,15 @@ class AdminHook(DashboardItemHook):
DashboardItemHook.__init__( DashboardItemHook.__init__(
self, self,
dashboard_admin, dashboard_admin,
1
)
class ESICheckHook(DashboardItemHook):
def __init__(self):
DashboardItemHook.__init__(
self,
dashboard_esi_check,
0 0
) )
@ -43,3 +52,8 @@ def register_groups_hook():
@hooks.register('dashboard_hook') @hooks.register('dashboard_hook')
def register_admin_hook(): def register_admin_hook():
return AdminHook() return AdminHook()
@hooks.register('dashboard_hook')
def register_esi_hook():
return ESICheckHook()

View File

@ -0,0 +1,12 @@
from django.utils.translation import gettext_lazy as _
# Overide ESI messages in the dashboard widget
# when the returned messages are not helpful or out of date
ESI_ERROR_MESSAGE_OVERRIDES = {
420: _("This software has exceeded the error limit for ESI. "
"If you are a user, please contact the maintainer of this software."
" If you are a developer/maintainer, please make a greater "
"effort in the future to receive valid responses. For tips on how, "
"come have a chat with us in ##3rd-party-dev-and-esi on the EVE "
"Online Discord. https://www.eveonline.com/discord")
}

View File

@ -1,10 +1,12 @@
import json import json
import requests_mock
from unittest.mock import patch from unittest.mock import patch
from django.test import RequestFactory, TestCase from django.test import RequestFactory, TestCase
from allianceauth.authentication.views import task_counts from allianceauth.authentication.views import task_counts, esi_check
from allianceauth.tests.auth_utils import AuthUtils from allianceauth.tests.auth_utils import AuthUtils
from allianceauth.authentication.constants import ESI_ERROR_MESSAGE_OVERRIDES
MODULE_PATH = "allianceauth.authentication.views" MODULE_PATH = "allianceauth.authentication.views"
@ -21,6 +23,8 @@ class TestRunningTasksCount(TestCase):
super().setUpClass() super().setUpClass()
cls.factory = RequestFactory() cls.factory = RequestFactory()
cls.user = AuthUtils.create_user("bruce_wayne") cls.user = AuthUtils.create_user("bruce_wayne")
cls.user.is_superuser = True
cls.user.save()
def test_should_return_data( def test_should_return_data(
self, mock_active_tasks_count, mock_queued_tasks_count self, mock_active_tasks_count, mock_queued_tasks_count
@ -35,5 +39,164 @@ class TestRunningTasksCount(TestCase):
# then # then
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertDictEqual( self.assertDictEqual(
jsonresponse_to_dict(response), {"tasks_running": 2, "tasks_queued": 3} jsonresponse_to_dict(response), {
"tasks_running": 2, "tasks_queued": 3}
) )
def test_su_only(
self, mock_active_tasks_count, mock_queued_tasks_count
):
self.user.is_superuser = False
self.user.save()
self.user.refresh_from_db()
# given
mock_active_tasks_count.return_value = 2
mock_queued_tasks_count.return_value = 3
request = self.factory.get("/")
request.user = self.user
# when
response = task_counts(request)
# then
self.assertEqual(response.status_code, 302)
class TestEsiCheck(TestCase):
@classmethod
def setUpClass(cls) -> None:
super().setUpClass()
cls.factory = RequestFactory()
cls.user = AuthUtils.create_user("bruce_wayne")
cls.user.is_superuser = True
cls.user.save()
@requests_mock.Mocker()
def test_401_data_returns_200(
self, m
):
error_json = {
"error": "You have been banned from using ESI. Please contact Technical Support. (support@eveonline.com)"
}
status_code = 401
m.get(
"https://esi.evetech.net/latest/status/?datasource=tranquility",
text=json.dumps(error_json),
status_code=status_code
)
# given
request = self.factory.get("/")
request.user = self.user
# when
response = esi_check(request)
# then
self.assertEqual(response.status_code, 200)
self.assertDictEqual(
jsonresponse_to_dict(response), {
"status": status_code,
"data": error_json
}
)
@requests_mock.Mocker()
def test_504_data_returns_200(
self, m
):
error_json = {
"error": "Gateway timeout message",
"timeout": 5000
}
status_code = 504
m.get(
"https://esi.evetech.net/latest/status/?datasource=tranquility",
text=json.dumps(error_json),
status_code=status_code
)
# given
request = self.factory.get("/")
request.user = self.user
# when
response = esi_check(request)
# then
self.assertEqual(response.status_code, 200)
self.assertDictEqual(
jsonresponse_to_dict(response), {
"status": status_code,
"data": error_json
}
)
@requests_mock.Mocker()
def test_420_data_override(
self, m
):
error_json = {
"error": "message from CCP",
}
status_code = 420
m.get(
"https://esi.evetech.net/latest/status/?datasource=tranquility",
text=json.dumps(error_json),
status_code=status_code
)
# given
request = self.factory.get("/")
request.user = self.user
# when
response = esi_check(request)
# then
self.assertEqual(response.status_code, 200)
self.assertNotEqual(
jsonresponse_to_dict(response)["data"],
error_json
)
self.assertDictEqual(
jsonresponse_to_dict(response), {
"status": status_code,
"data": {
"error": ESI_ERROR_MESSAGE_OVERRIDES.get(status_code)
}
}
)
@requests_mock.Mocker()
def test_200_data_returns_200(
self, m
):
good_json = {
"players": 5,
"server_version": "69420",
"start_time": "2030-01-01T23:59:59Z"
}
status_code = 200
m.get(
"https://esi.evetech.net/latest/status/?datasource=tranquility",
text=json.dumps(good_json),
status_code=status_code
)
# given
request = self.factory.get("/")
request.user = self.user
# when
response = esi_check(request)
# then
self.assertEqual(response.status_code, 200)
self.assertDictEqual(
jsonresponse_to_dict(response), {
"status": status_code,
"data": good_json
}
)
def test_su_only(
self,
):
self.user.is_superuser = False
self.user.save()
self.user.refresh_from_db()
# given
request = self.factory.get("/")
request.user = self.user
# when
response = esi_check(request)
# then
self.assertEqual(response.status_code, 302)

View File

@ -40,4 +40,5 @@ urlpatterns = [
path('dashboard/', views.dashboard, name='dashboard'), path('dashboard/', views.dashboard, name='dashboard'),
path('dashboard_bs3/', views.dashboard_bs3, name='dashboard_bs3'), path('dashboard_bs3/', views.dashboard_bs3, name='dashboard_bs3'),
path('task-counts/', views.task_counts, name='task_counts'), path('task-counts/', views.task_counts, name='task_counts'),
path('esi-check/', views.esi_check, name='esi_check'),
] ]

View File

@ -1,4 +1,7 @@
import logging import logging
import requests
from allianceauth.hooks import get_hooks
from django_registration.backends.activation.views import ( from django_registration.backends.activation.views import (
REGISTRATION_SALT, ActivationView as BaseActivationView, REGISTRATION_SALT, ActivationView as BaseActivationView,
@ -9,7 +12,7 @@ from django_registration.signals import user_registered
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.contrib.auth import authenticate, login from django.contrib.auth import authenticate, login
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required, user_passes_test
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core import signing from django.core import signing
from django.http import JsonResponse from django.http import JsonResponse
@ -24,6 +27,7 @@ from esi.models import Token
from allianceauth.eveonline.models import EveCharacter from allianceauth.eveonline.models import EveCharacter
from allianceauth.hooks import get_hooks from allianceauth.hooks import get_hooks
from .constants import ESI_ERROR_MESSAGE_OVERRIDES
from .core.celery_workers import active_tasks_count, queued_tasks_count from .core.celery_workers import active_tasks_count, queued_tasks_count
from .forms import RegistrationForm from .forms import RegistrationForm
from .models import CharacterOwnership from .models import CharacterOwnership
@ -76,6 +80,13 @@ def dashboard_admin(request):
return "" return ""
def dashboard_esi_check(request):
if request.user.is_superuser:
return render_to_string('allianceauth/admin-status/esi_check.html', request=request)
else:
return ""
@login_required @login_required
def dashboard(request): def dashboard(request):
_dash_items = list() _dash_items = list()
@ -135,23 +146,28 @@ def token_refresh(request, token_id=None):
@login_required @login_required
@token_required(scopes=settings.LOGIN_TOKEN_SCOPES) @token_required(scopes=settings.LOGIN_TOKEN_SCOPES)
def main_character_change(request, token): def main_character_change(request, token):
logger.debug(f"main_character_change called by user {request.user} for character {token.character_name}") logger.debug(
f"main_character_change called by user {request.user} for character {token.character_name}")
try: try:
co = CharacterOwnership.objects.get(character__character_id=token.character_id, user=request.user) co = CharacterOwnership.objects.get(
character__character_id=token.character_id, user=request.user)
except CharacterOwnership.DoesNotExist: except CharacterOwnership.DoesNotExist:
if not CharacterOwnership.objects.filter(character__character_id=token.character_id).exists(): if not CharacterOwnership.objects.filter(character__character_id=token.character_id).exists():
co = CharacterOwnership.objects.create_by_token(token) co = CharacterOwnership.objects.create_by_token(token)
else: else:
messages.error( messages.error(
request, request,
_('Cannot change main character to %(char)s: character owned by a different account.') % ({'char': token.character_name}) _('Cannot change main character to %(char)s: character owned by a different account.') % (
{'char': token.character_name})
) )
co = None co = None
if co: if co:
request.user.profile.main_character = co.character request.user.profile.main_character = co.character
request.user.profile.save(update_fields=['main_character']) request.user.profile.save(update_fields=['main_character'])
messages.success(request, _('Changed main character to %(char)s') % {"char": co.character}) messages.success(request, _('Changed main character to %(char)s') % {
logger.info('Changed user %(user)s main character to %(char)s' % ({'user': request.user, 'char': co.character})) "char": co.character})
logger.info('Changed user %(user)s main character to %(char)s' %
({'user': request.user, 'char': co.character}))
return redirect("authentication:dashboard") return redirect("authentication:dashboard")
@ -159,9 +175,11 @@ def main_character_change(request, token):
def add_character(request, token): def add_character(request, token):
if CharacterOwnership.objects.filter(character__character_id=token.character_id).filter( if CharacterOwnership.objects.filter(character__character_id=token.character_id).filter(
owner_hash=token.character_owner_hash).filter(user=request.user).exists(): owner_hash=token.character_owner_hash).filter(user=request.user).exists():
messages.success(request, _('Added %(name)s to your account.' % ({'name': token.character_name}))) messages.success(request, _(
'Added %(name)s to your account.' % ({'name': token.character_name})))
else: else:
messages.error(request, _('Failed to add %(name)s to your account: they already have an account.' % ({'name': token.character_name}))) messages.error(request, _('Failed to add %(name)s to your account: they already have an account.' % (
{'name': token.character_name})))
return redirect('authentication:dashboard') return redirect('authentication:dashboard')
@ -278,7 +296,8 @@ class RegistrationView(BaseRegistrationView):
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
def register(self, form): def register(self, form):
user = User.objects.get(pk=self.request.session.get('registration_uid')) user = User.objects.get(
pk=self.request.session.get('registration_uid'))
user.email = form.cleaned_data['email'] user.email = form.cleaned_data['email']
user_registered.send(self.__class__, user=user, request=self.request) user_registered.send(self.__class__, user=user, request=self.request)
if getattr(settings, 'REGISTRATION_VERIFY_EMAIL', True): if getattr(settings, 'REGISTRATION_VERIFY_EMAIL', True):
@ -287,7 +306,8 @@ class RegistrationView(BaseRegistrationView):
else: else:
user.is_active = True user.is_active = True
user.save() user.save()
login(self.request, user, 'allianceauth.authentication.backends.StateBackend') login(self.request, user,
'allianceauth.authentication.backends.StateBackend')
return user return user
def get_activation_key(self, user): def get_activation_key(self, user):
@ -295,7 +315,8 @@ class RegistrationView(BaseRegistrationView):
def get_email_context(self, activation_key): def get_email_context(self, activation_key):
context = super().get_email_context(activation_key) context = super().get_email_context(activation_key)
context['url'] = context['site'].domain + reverse('registration_activate', args=[activation_key]) context['url'] = context['site'].domain + \
reverse('registration_activate', args=[activation_key])
return context return context
@ -328,20 +349,24 @@ class ActivationView(BaseActivationView):
def registration_complete(request): def registration_complete(request):
messages.success(request, _('Sent confirmation email. Please follow the link to confirm your email address.')) messages.success(request, _(
'Sent confirmation email. Please follow the link to confirm your email address.'))
return redirect('authentication:login') return redirect('authentication:login')
def activation_complete(request): def activation_complete(request):
messages.success(request, _('Confirmed your email address. Please login to continue.')) messages.success(request, _(
'Confirmed your email address. Please login to continue.'))
return redirect('authentication:dashboard') return redirect('authentication:dashboard')
def registration_closed(request): def registration_closed(request):
messages.error(request, _('Registration of new accounts is not allowed at this time.')) messages.error(request, _(
'Registration of new accounts is not allowed at this time.'))
return redirect('authentication:login') return redirect('authentication:login')
@user_passes_test(lambda u: u.is_superuser)
def task_counts(request) -> JsonResponse: def task_counts(request) -> JsonResponse:
"""Return task counts as JSON for an AJAX call.""" """Return task counts as JSON for an AJAX call."""
data = { data = {
@ -351,6 +376,25 @@ def task_counts(request) -> JsonResponse:
return JsonResponse(data) return JsonResponse(data)
def check_for_override_esi_error_message(response):
if response.status_code in ESI_ERROR_MESSAGE_OVERRIDES:
return {"error": ESI_ERROR_MESSAGE_OVERRIDES.get(response.status_code)}
else:
return response.json()
@user_passes_test(lambda u: u.is_superuser)
def esi_check(request) -> JsonResponse:
"""Return if ESI ok With error messages and codes as JSON"""
_r = requests.get("https://esi.evetech.net/latest/status/?datasource=tranquility")
data = {
"status": _r.status_code,
"data": check_for_override_esi_error_message(_r)
}
return JsonResponse(data)
@login_required @login_required
def dashboard_bs3(request): def dashboard_bs3(request):
"""Render dashboard view with BS3 theme. """Render dashboard view with BS3 theme.

View File

@ -0,0 +1,37 @@
{% load i18n %}
<div id="esi-alert" class="col-12 align-self-stretch py-2 collapse">
<div class="alert alert-warning">
<p class="text-center ">{% translate 'Your Server received an ESI error response code of ' %}<b id="esi-code">?</b></p>
<hr>
<pre id="esi-data" class="text-center text-wrap"></pre>
</div>
</div>
<script>
const elemCard = document.getElementById("esi-alert");
const elemMessage = document.getElementById("esi-data");
const elemCode = document.getElementById("esi-code");
fetch('{% url "authentication:esi_check" %}')
.then((response) => {
if (response.ok) {
return response.json();
}
throw new Error("Something went wrong");
})
.then((responseJson) => {
console.log("ESI Check: ", JSON.stringify(responseJson, null, 2));
const status = responseJson.status;
if (status != 200) {
elemCode.textContent = status
elemMessage.textContent = responseJson.data.error;
new bootstrap.Collapse(elemCard, {
toggle: true
})
}
})
.catch((error) => {
console.log(error);
});
</script>