Compare commits

...

28 Commits

Author SHA1 Message Date
Ariel Rin
def6431052 Version Bump 2.14.0 2022-07-11 14:27:49 +10:00
Ariel Rin
22a270aedb Merge branch 'filterdropdown-backwards-compatibility' into 'master'
Add filterdropdown bundle to AA2 to ensure backwards compatibility

See merge request allianceauth/allianceauth!1437
2022-07-11 04:15:25 +00:00
Peter Pfeufer
c930f7bbeb Also adds timers.js, eve-time.js and refresh_notifications.js
As these seem to be used in some apps as well
2022-07-09 15:57:43 +02:00
Peter Pfeufer
64ee273953 Add filterdropdown bundle to AA2 to ensure backwards compatibility 2022-07-09 13:43:05 +02:00
Ariel Rin
3706a1aedf Merge branch 'improve-autodocs-for-models' into 'master'
Improve autodocs for models & more

See merge request allianceauth/allianceauth!1435
2022-07-07 07:38:58 +00:00
Ariel Rin
47f1b77320 Merge branch 'consolidate-redis-client-access' into 'master'
Ensure backwards compatibility when fetching a redis client

See merge request allianceauth/allianceauth!1428
2022-07-07 07:37:21 +00:00
Erik Kalkoken
8dec242a93 Ensure backwards compatibility when fetching a redis client 2022-07-07 07:37:21 +00:00
ErikKalkoken
2ff200c566 Refer to django-esi docs 2022-06-27 13:43:45 +02:00
ErikKalkoken
091a2637ea Add extension to improve autodocs for Django models & enable source links 2022-06-27 13:41:15 +02:00
Ariel Rin
113977b19f Version Bump 2.13.0 2022-06-18 13:07:36 +10:00
Ariel Rin
8f39b50b6d Merge branch 'Maestro-Zacht-fix-fat-attributeerror' into 'master'
fixed attribute error

See merge request allianceauth/allianceauth!1421
2022-06-18 02:53:11 +00:00
Maestro-Zacht
95b309c358 fixed attribute error 2022-06-18 02:53:11 +00:00
Ariel Rin
cf3df3b715 Merge branch 'fix_issue_1328' into 'master'
Fix: Changing group's state setting does not kick existing non-conforming group members

Closes #1328

See merge request allianceauth/allianceauth!1400
2022-06-18 02:47:14 +00:00
Erik Kalkoken
d815028c4d Fix: Changing group's state setting does not kick existing non-conforming group members 2022-06-18 02:47:14 +00:00
Ariel Rin
ac5570abe2 Merge branch 'fix_issue_1268' into 'master'
Fix: Service group updates broken when adding users to groups

Closes #1268

See merge request allianceauth/allianceauth!1403
2022-06-18 02:41:23 +00:00
Erik Kalkoken
84ad571aa4 Fix: Service group updates broken when adding users to groups 2022-06-18 02:41:23 +00:00
Ariel Rin
38e7705ae7 Merge branch 'docs-dark-mode' into 'master'
Add automatic dark mode to docs

See merge request allianceauth/allianceauth!1427
2022-06-18 02:39:59 +00:00
ErikKalkoken
0b6af014fa Add automatic dark mode to docs 2022-06-17 21:49:18 +02:00
Ariel Rin
2401f2299d Merge branch 'fix-doc-redis-issue' into 'master'
Fix: Broken docs generation on readthedocs.org (2nd attempt)

See merge request allianceauth/allianceauth!1425
2022-06-17 11:58:45 +00:00
Erik Kalkoken
919768c8bb Fix: Broken docs generation on readthedocs.org (2nd attempt) 2022-06-17 11:58:45 +00:00
Ariel Rin
24db21463b Merge branch 'docs-template-tags-example' into 'master'
Add example for template tags to docs

See merge request allianceauth/allianceauth!1426
2022-06-17 11:58:05 +00:00
Erik Kalkoken
1e029af83a Add example for template tags to docs 2022-06-17 11:58:05 +00:00
Ariel Rin
2b31be789d Merge branch 'fix-issue-1336' into 'master'
Fix: Broken docs generation on readthedocs.org

Closes #1336

See merge request allianceauth/allianceauth!1423
2022-06-06 10:48:16 +00:00
Erik Kalkoken
bf1b4bb549 Fix: Broken docs generation on readthedocs.org 2022-06-06 10:48:16 +00:00
Ariel Rin
dd42b807f0 Version Bump 2.12.1 2022-05-13 00:19:45 +10:00
Ariel Rin
542fbafd98 Merge branch 'cherry-pick-4836559a' into 'v2.12.x'
Merge branch 'fix-decimal_widthratio-template-tag' into 'v2.12.x'

See merge request allianceauth/allianceauth!1420
2022-05-12 14:14:01 +00:00
Ariel Rin
37b9f5c882 Merge branch 'fix-decimal_widthratio-template-tag' into 'v3.x'
[FIX] Division by zero in decimal_widthratio template tag

See merge request allianceauth/allianceauth!1419

(cherry picked from commit 4836559abe)

8dd07b97 [FIX] Devision by zero in decimal_widthratio template tag
17b06c88 Make it a string in accordance to the return value type
2022-05-12 13:33:45 +00:00
Ariel Rin
5bde9a6952 Version Bump 2.12.0 2022-05-12 18:54:22 +10:00
41 changed files with 743 additions and 194 deletions

View File

@@ -168,6 +168,12 @@ test-3.11-all:
path: coverage.xml
allow_failure: true
test-docs:
<<: *only-default
image: python:3.9-bullseye
script:
- tox -e docs
deploy_production:
stage: deploy
image: python:3.10-bullseye

View File

@@ -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.11.2'
__version__ = '2.14.0'
__title__ = 'Alliance Auth'
__url__ = 'https://gitlab.com/allianceauth/allianceauth'
NAME = f'{__title__} v{__version__}'

View File

@@ -1,27 +1,62 @@
import datetime as dt
from typing import Optional, List
import logging
from typing import List, Optional
from redis import Redis
from pytz import utc
from redis import Redis, RedisError
from django.core.cache import cache
from allianceauth.utils.cache import get_redis_client
logger = logging.getLogger(__name__)
class _RedisStub:
"""Stub of a Redis client.
It's purpose is to prevent EventSeries objects from trying to access Redis
when it is not available. e.g. when the Sphinx docs are rendered by readthedocs.org.
"""
def delete(self, *args, **kwargs):
pass
def incr(self, *args, **kwargs):
return 0
def zadd(self, *args, **kwargs):
pass
def zcount(self, *args, **kwargs):
pass
def zrangebyscore(self, *args, **kwargs):
pass
class EventSeries:
"""API for recording and analysing a series of events."""
"""API for recording and analyzing 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._redis = get_redis_client() if not redis else redis
try:
if not self._redis.ping():
raise RuntimeError()
except (AttributeError, RedisError, RuntimeError):
logger.exception(
"Failed to establish a connection with Redis. "
"This EventSeries object is disabled.",
)
self._redis = _RedisStub()
self._key_id = str(key_id)
self.clear()
@property
def is_disabled(self):
"""True when this object is disabled, e.g. Redis was not available at startup."""
return isinstance(self._redis, _RedisStub)
@property
def _key_counter(self):
return f"{self._ROOT_KEY}_{self._key_id}_COUNTER"

View File

@@ -1,13 +1,48 @@
import datetime as dt
from unittest.mock import patch
from pytz import utc
from redis import RedisError
from django.test import TestCase
from django.utils.timezone import now
from allianceauth.authentication.task_statistics.event_series import EventSeries
from allianceauth.authentication.task_statistics.event_series import (
EventSeries,
_RedisStub,
)
MODULE_PATH = "allianceauth.authentication.task_statistics.event_series"
class TestEventSeries(TestCase):
def test_should_abort_without_redis_client(self):
# when
with patch(MODULE_PATH + ".get_redis_client") as mock:
mock.return_value = None
events = EventSeries("dummy")
# then
self.assertTrue(events._redis, _RedisStub)
self.assertTrue(events.is_disabled)
def test_should_disable_itself_if_redis_not_available_1(self):
# when
with patch(MODULE_PATH + ".get_redis_client") as mock_get_master_client:
mock_get_master_client.return_value.ping.side_effect = RedisError
events = EventSeries("dummy")
# then
self.assertIsInstance(events._redis, _RedisStub)
self.assertTrue(events.is_disabled)
def test_should_disable_itself_if_redis_not_available_2(self):
# when
with patch(MODULE_PATH + ".get_redis_client") as mock_get_master_client:
mock_get_master_client.return_value.ping.return_value = False
events = EventSeries("dummy")
# then
self.assertIsInstance(events._redis, _RedisStub)
self.assertTrue(events.is_disabled)
def test_should_add_event(self):
# given
events = EventSeries("dummy")

View File

@@ -212,7 +212,14 @@ def fatlink_monthly_personal_statistics_view(request, year, month, char_id=None)
start_of_previous_month = first_day_of_previous_month(year, month)
if request.user.has_perm('auth.fleetactivitytracking_statistics') and char_id:
user = EveCharacter.objects.get(character_id=char_id).user
try:
user = EveCharacter.objects.get(character_id=char_id).character_ownership.user
except EveCharacter.DoesNotExist:
messages.error(request, _('Character does not exist'))
return redirect('fatlink:view')
except AttributeError:
messages.error(request, _('User does not exist'))
return redirect('fatlink:view')
else:
user = request.user
logger.debug(f"Personal monthly statistics view for user {user} called by {request.user}")

View File

@@ -1,8 +1,8 @@
from django.apps import apps
from django.contrib import admin
from django.contrib.auth.models import Group as BaseGroup
from django.contrib.auth.models import Permission, User
from django.db.models import Count
from django.contrib.auth.models import Group as BaseGroup, Permission, User
from django.db.models import Count, Exists, OuterRef
from django.db.models.functions import Lower
from django.db.models.signals import (
m2m_changed,
@@ -15,6 +15,7 @@ from django.dispatch import receiver
from .forms import GroupAdminForm, ReservedGroupNameAdminForm
from .models import AuthGroup, GroupRequest, ReservedGroupName
from .tasks import remove_users_not_matching_states_from_group
if 'eve_autogroups' in apps.app_configs:
_has_auto_groups = True
@@ -106,14 +107,13 @@ class HasLeaderFilter(admin.SimpleListFilter):
class GroupAdmin(admin.ModelAdmin):
form = GroupAdminForm
list_select_related = ('authgroup',)
ordering = ('name',)
list_display = (
'name',
'_description',
'_properties',
'_member_count',
'has_leader'
'has_leader',
)
list_filter = [
'authgroup__internal',
@@ -129,31 +129,51 @@ class GroupAdmin(admin.ModelAdmin):
def get_queryset(self, request):
qs = super().get_queryset(request)
if _has_auto_groups:
qs = qs.prefetch_related('managedalliancegroup_set', 'managedcorpgroup_set')
qs = qs.prefetch_related('authgroup__group_leaders').select_related('authgroup')
qs = qs.annotate(
member_count=Count('user', distinct=True),
has_leader_qs = (
AuthGroup.objects.filter(group=OuterRef('pk'), group_leaders__isnull=False)
)
has_leader_groups_qs = (
AuthGroup.objects.filter(
group=OuterRef('pk'), group_leader_groups__isnull=False
)
)
qs = (
qs.select_related('authgroup')
.annotate(member_count=Count('user', distinct=True))
.annotate(has_leader=Exists(has_leader_qs))
.annotate(has_leader_groups=Exists(has_leader_groups_qs))
)
if _has_auto_groups:
is_autogroup_corp = (
Group.objects.filter(
pk=OuterRef('pk'), managedcorpgroup__isnull=False
)
)
is_autogroup_alliance = (
Group.objects.filter(
pk=OuterRef('pk'), managedalliancegroup__isnull=False
)
)
qs = (
qs.annotate(is_autogroup_corp=Exists(is_autogroup_corp))
.annotate(is_autogroup_alliance=Exists(is_autogroup_alliance))
)
return qs
def _description(self, obj):
return obj.authgroup.description
@admin.display(description="Members", ordering="member_count")
@admin.display(description='Members', ordering='member_count')
def _member_count(self, obj):
return obj.member_count
@admin.display(boolean=True)
def has_leader(self, obj):
return obj.authgroup.group_leaders.exists() or obj.authgroup.group_leader_groups.exists()
return obj.has_leader or obj.has_leader_groups
def _properties(self, obj):
properties = list()
if _has_auto_groups and (
obj.managedalliancegroup_set.exists()
or obj.managedcorpgroup_set.exists()
):
if _has_auto_groups and (obj.is_autogroup_corp or obj.is_autogroup_alliance):
properties.append('Auto Group')
elif obj.authgroup.internal:
properties.append('Internal')
@@ -183,6 +203,8 @@ class GroupAdmin(admin.ModelAdmin):
ag_instance = inline_form.save(commit=False)
ag_instance.group = form.instance
ag_instance.save()
if ag_instance.states.exists():
remove_users_not_matching_states_from_group.delay(ag_instance.group.pk)
formset.save()
def get_readonly_fields(self, request, obj=None):

View File

@@ -189,6 +189,15 @@ class AuthGroup(models.Model):
| User.objects.filter(groups__in=list(self.group_leader_groups.all()))
)
def remove_users_not_matching_states(self):
"""Remove users not matching defined states from related group."""
states_qs = self.states.all()
if states_qs.exists():
states = list(states_qs)
non_compliant_users = self.group.user_set.exclude(profile__state__in=states)
for user in non_compliant_users:
self.group.user_set.remove(user)
class ReservedGroupName(models.Model):
"""Name that can not be used for groups.

View File

@@ -0,0 +1,10 @@
from celery import shared_task
from django.contrib.auth.models import Group
@shared_task
def remove_users_not_matching_states_from_group(group_pk: int) -> None:
"""Remove users not matching defined states from related group."""
group = Group.objects.get(pk=group_pk)
group.authgroup.remove_users_not_matching_states()

View File

@@ -6,7 +6,7 @@ from django.conf import settings
from django.contrib import admin
from django.contrib.admin.sites import AdminSite
from django.contrib.auth.models import User
from django.test import TestCase, RequestFactory, Client
from django.test import TestCase, RequestFactory, Client, override_settings
from allianceauth.authentication.models import CharacterOwnership, State
from allianceauth.eveonline.models import (
@@ -236,60 +236,104 @@ class TestGroupAdmin(TestCase):
self.assertEqual(result, expected)
def test_member_count(self):
expected = 1
obj = self.modeladmin.get_queryset(MockRequest(user=self.user_1))\
.get(pk=self.group_1.pk)
# given
request = MockRequest(user=self.user_1)
obj = self.modeladmin.get_queryset(request).get(pk=self.group_1.pk)
# when
result = self.modeladmin._member_count(obj)
self.assertEqual(result, expected)
# then
self.assertEqual(result, 1)
def test_has_leader_user(self):
result = self.modeladmin.has_leader(self.group_1)
# given
request = MockRequest(user=self.user_1)
obj = self.modeladmin.get_queryset(request).get(pk=self.group_1.pk)
# when
result = self.modeladmin.has_leader(obj)
# then
self.assertTrue(result)
def test_has_leader_group(self):
result = self.modeladmin.has_leader(self.group_2)
# given
request = MockRequest(user=self.user_1)
obj = self.modeladmin.get_queryset(request).get(pk=self.group_2.pk)
# when
result = self.modeladmin.has_leader(obj)
# then
self.assertTrue(result)
def test_properties_1(self):
expected = ['Default']
result = self.modeladmin._properties(self.group_1)
self.assertListEqual(result, expected)
# given
request = MockRequest(user=self.user_1)
obj = self.modeladmin.get_queryset(request).get(pk=self.group_1.pk)
# when
result = self.modeladmin._properties(obj)
self.assertListEqual(result, ['Default'])
def test_properties_2(self):
expected = ['Internal']
result = self.modeladmin._properties(self.group_2)
self.assertListEqual(result, expected)
# given
request = MockRequest(user=self.user_1)
obj = self.modeladmin.get_queryset(request).get(pk=self.group_2.pk)
# when
result = self.modeladmin._properties(obj)
self.assertListEqual(result, ['Internal'])
def test_properties_3(self):
expected = ['Hidden']
result = self.modeladmin._properties(self.group_3)
self.assertListEqual(result, expected)
# given
request = MockRequest(user=self.user_1)
obj = self.modeladmin.get_queryset(request).get(pk=self.group_3.pk)
# when
result = self.modeladmin._properties(obj)
self.assertListEqual(result, ['Hidden'])
def test_properties_4(self):
expected = ['Open']
result = self.modeladmin._properties(self.group_4)
self.assertListEqual(result, expected)
# given
request = MockRequest(user=self.user_1)
obj = self.modeladmin.get_queryset(request).get(pk=self.group_4.pk)
# when
result = self.modeladmin._properties(obj)
self.assertListEqual(result, ['Open'])
def test_properties_5(self):
expected = ['Public']
result = self.modeladmin._properties(self.group_5)
self.assertListEqual(result, expected)
# given
request = MockRequest(user=self.user_1)
obj = self.modeladmin.get_queryset(request).get(pk=self.group_5.pk)
# when
result = self.modeladmin._properties(obj)
self.assertListEqual(result, ['Public'])
def test_properties_6(self):
expected = ['Hidden', 'Open', 'Public']
result = self.modeladmin._properties(self.group_6)
self.assertListEqual(result, expected)
# given
request = MockRequest(user=self.user_1)
obj = self.modeladmin.get_queryset(request).get(pk=self.group_6.pk)
# when
result = self.modeladmin._properties(obj)
self.assertListEqual(result, ['Hidden', 'Open', 'Public'])
if _has_auto_groups:
@patch(MODULE_PATH + '._has_auto_groups', True)
def test_properties_7(self):
def test_should_show_autogroup_for_corporation(self):
# given
self._create_autogroups()
expected = ['Auto Group']
my_group = Group.objects\
.filter(managedcorpgroup__isnull=False)\
.first()
result = self.modeladmin._properties(my_group)
self.assertListEqual(result, expected)
request = MockRequest(user=self.user_1)
queryset = self.modeladmin.get_queryset(request)
obj = queryset.filter(managedcorpgroup__isnull=False).first()
# when
result = self.modeladmin._properties(obj)
# then
self.assertListEqual(result, ['Auto Group'])
@patch(MODULE_PATH + '._has_auto_groups', True)
def test_should_show_autogroup_for_alliance(self):
# given
self._create_autogroups()
request = MockRequest(user=self.user_1)
queryset = self.modeladmin.get_queryset(request)
obj = queryset.filter(managedalliancegroup__isnull=False).first()
# when
result = self.modeladmin._properties(obj)
# then
self.assertListEqual(result, ['Auto Group'])
# actions
@@ -539,6 +583,68 @@ class TestGroupAdminChangeFormSuperuserExclusiveEdits(WebTest):
self.assertNotIn(field, form.fields)
@override_settings(CELERY_ALWAYS_EAGER=True, CELERY_EAGER_PROPAGATES_EXCEPTIONS=True)
class TestGroupAdmin2(TestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.superuser = User.objects.create_superuser("super")
def test_should_remove_users_from_state_groups(self):
# given
user_member = AuthUtils.create_user("Bruce Wayne")
character_member = AuthUtils.add_main_character_2(
user_member,
name="Bruce Wayne",
character_id=1001,
corp_id=2001,
corp_name="Wayne Technologies",
)
user_guest = AuthUtils.create_user("Lex Luthor")
AuthUtils.add_main_character_2(
user_guest,
name="Lex Luthor",
character_id=1011,
corp_id=2011,
corp_name="Luthor Corp",
)
member_state = AuthUtils.get_member_state()
member_state.member_characters.add(character_member)
user_member.refresh_from_db()
user_guest.refresh_from_db()
group = Group.objects.create(name="dummy")
user_member.groups.add(group)
user_guest.groups.add(group)
group.authgroup.states.add(member_state)
self.client.force_login(self.superuser)
# when
response = self.client.post(
f"/admin/groupmanagement/group/{group.pk}/change/",
data={
"name": f"{group.name}",
"authgroup-TOTAL_FORMS": "1",
"authgroup-INITIAL_FORMS": "1",
"authgroup-MIN_NUM_FORMS": "0",
"authgroup-MAX_NUM_FORMS": "1",
"authgroup-0-description": "",
"authgroup-0-states": f"{member_state.pk}",
"authgroup-0-internal": "on",
"authgroup-0-hidden": "on",
"authgroup-0-group": f"{group.pk}",
"authgroup-__prefix__-description": "",
"authgroup-__prefix__-internal": "on",
"authgroup-__prefix__-hidden": "on",
"authgroup-__prefix__-group": f"{group.pk}",
"_save": "Save"
}
)
# then
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, "/admin/groupmanagement/group/")
self.assertIn(group, user_member.groups.all())
self.assertNotIn(group, user_guest.groups.all())
class TestReservedGroupNameAdmin(TestCase):
@classmethod
def setUpClass(cls):

View File

@@ -232,6 +232,38 @@ class TestAuthGroup(TestCase):
expected = 'Superheros'
self.assertEqual(str(group.authgroup), expected)
def test_should_remove_guests_from_group_when_restricted_to_members_only(self):
# given
user_member = AuthUtils.create_user("Bruce Wayne")
character_member = AuthUtils.add_main_character_2(
user_member,
name="Bruce Wayne",
character_id=1001,
corp_id=2001,
corp_name="Wayne Technologies",
)
user_guest = AuthUtils.create_user("Lex Luthor")
AuthUtils.add_main_character_2(
user_guest,
name="Lex Luthor",
character_id=1011,
corp_id=2011,
corp_name="Luthor Corp",
)
member_state = AuthUtils.get_member_state()
member_state.member_characters.add(character_member)
user_member.refresh_from_db()
user_guest.refresh_from_db()
group = Group.objects.create(name="dummy")
user_member.groups.add(group)
user_guest.groups.add(group)
group.authgroup.states.add(member_state)
# when
group.authgroup.remove_users_not_matching_states()
# then
self.assertIn(group, user_member.groups.all())
self.assertNotIn(group, user_guest.groups.all())
class TestAuthGroupRequestApprovers(TestCase):
def setUp(self) -> None:

View File

@@ -8,7 +8,7 @@ from uuid import uuid1
from redis import Redis
import requests
from django.core.cache import caches
from allianceauth.utils.cache import get_redis_client
from allianceauth import __title__ as AUTH_TITLE, __url__, __version__
@@ -103,8 +103,7 @@ class DiscordClient:
self._access_token = str(access_token)
self._is_rate_limited = bool(is_rate_limited)
if not redis:
default_cache = caches['default']
self._redis = default_cache.get_master_client()
self._redis = get_redis_client()
if not isinstance(self._redis, Redis):
raise RuntimeError(
'This class requires a Redis client, but none was provided '

View File

@@ -85,29 +85,18 @@ class TestBasicsAndHelpers(TestCase):
client = DiscordClient(TEST_BOT_TOKEN, mock_redis, is_rate_limited=True)
self.assertTrue(client.is_rate_limited)
@patch(MODULE_PATH + '.caches')
def test_use_default_redis_if_none_provided(self, mock_caches):
my_redis = MagicMock(spec=Redis)
mock_default_cache = MagicMock(**{'get_master_client.return_value': my_redis})
my_dict = {'default': mock_default_cache}
mock_caches.__getitem__.side_effect = my_dict.__getitem__
def test_use_default_redis_if_none_provided(self):
client = DiscordClient(TEST_BOT_TOKEN)
self.assertTrue(mock_default_cache.get_master_client.called)
self.assertEqual(client._redis, my_redis)
@patch(MODULE_PATH + '.caches')
def test_raise_exception_if_default_cache_is_not_redis(self, mock_caches):
my_redis = MagicMock()
mock_default_cache = MagicMock(**{'get_master_client.return_value': my_redis})
my_dict = {'default': mock_default_cache}
mock_caches.__getitem__.side_effect = my_dict.__getitem__
self.assertIsInstance(client._redis, Redis)
@patch(MODULE_PATH + '.get_redis_client')
def test_raise_exception_if_redis_client_not_found(self, mock_get_redis_client):
# given
mock_get_redis_client.return_value = None
# when
with self.assertRaises(RuntimeError):
DiscordClient(TEST_BOT_TOKEN)
self.assertTrue(mock_default_cache.get_master_client.called)
@requests_mock.Mocker()
class TestOtherMethods(TestCase):

View File

@@ -35,17 +35,17 @@ import logging
from uuid import uuid1
import random
from django.core.cache import caches
from django.contrib.auth.models import User, Group
from allianceauth.services.modules.discord.models import DiscordUser
from allianceauth.utils.cache import get_redis_client
logger = logging.getLogger('allianceauth')
MAX_RUNS = 3
def clear_cache():
default_cache = caches['default']
redis = default_cache.get_master_client()
redis = get_redis_client()
redis.flushall()
logger.info('Cache flushed')

View File

@@ -14,7 +14,6 @@ from requests.exceptions import HTTPError
import requests_mock
from django.contrib.auth.models import Group, User
from django.core.cache import caches
from django.shortcuts import reverse
from django.test import TransactionTestCase, TestCase
from django.test.utils import override_settings
@@ -23,6 +22,7 @@ from allianceauth.authentication.models import State
from allianceauth.eveonline.models import EveCharacter
from allianceauth.notifications.models import Notification
from allianceauth.tests.auth_utils import AuthUtils
from allianceauth.utils.cache import get_redis_client
from . import (
TEST_GUILD_ID,
@@ -87,8 +87,7 @@ remove_guild_member_request = DiscordRequest(
def clear_cache():
default_cache = caches['default']
redis = default_cache.get_master_client()
redis = get_redis_client()
redis.flushall()
logger.info('Cache flushed')
@@ -109,7 +108,6 @@ def reset_testdata():
class TestServiceFeatures(TransactionTestCase):
fixtures = ['disable_analytics.json']
@classmethod
def setUpClass(cls):
super().setUpClass()

View File

@@ -1,4 +1,5 @@
import logging
from functools import partial
from django.contrib.auth.models import User, Group, Permission
from django.core.exceptions import ObjectDoesNotExist
@@ -8,7 +9,7 @@ from django.db.models.signals import pre_delete
from django.db.models.signals import pre_save
from django.dispatch import receiver
from .hooks import ServicesHook
from .tasks import disable_user
from .tasks import disable_user, update_groups_for_user
from allianceauth.authentication.models import State, UserProfile
from allianceauth.authentication.signals import state_changed
@@ -19,21 +20,27 @@ logger = logging.getLogger(__name__)
@receiver(m2m_changed, sender=User.groups.through)
def m2m_changed_user_groups(sender, instance, action, *args, **kwargs):
logger.debug(f"Received m2m_changed from {instance} groups with action {action}")
def trigger_service_group_update():
logger.debug("Triggering service group update for %s" % instance)
# Iterate through Service hooks
for svc in ServicesHook.get_services():
try:
svc.validate_user(instance)
svc.update_groups(instance)
except:
logger.exception(f'Exception running update_groups for services module {svc} on user {instance}')
if instance.pk and (action == "post_add" or action == "post_remove" or action == "post_clear"):
logger.debug("Waiting for commit to trigger service group update for %s" % instance)
transaction.on_commit(trigger_service_group_update)
logger.debug(
"%s: Received m2m_changed from groups with action %s", instance, action
)
if instance.pk and (
action == "post_add" or action == "post_remove" or action == "post_clear"
):
if isinstance(instance, User):
logger.debug(
"Waiting for commit to trigger service group update for %s", instance
)
transaction.on_commit(partial(update_groups_for_user.delay, instance.pk))
elif (
isinstance(instance, Group)
and kwargs.get("model") is User
and "pk_set" in kwargs
):
for user_pk in kwargs["pk_set"]:
logger.debug(
"%s: Waiting for commit to trigger service group update for user", user_pk
)
transaction.on_commit(partial(update_groups_for_user.delay, user_pk))
@receiver(m2m_changed, sender=User.user_permissions.through)

View File

@@ -47,3 +47,20 @@ def disable_user(user):
for svc in ServicesHook.get_services():
if svc.service_active_for_user(user):
svc.delete_user(user)
@shared_task
def update_groups_for_user(user_pk: int) -> None:
"""Update groups for all services registered to a user."""
user = User.objects.get(pk=user_pk)
logger.debug("%s: Triggering service group update for user", user)
for svc in ServicesHook.get_services():
try:
svc.validate_user(user)
svc.update_groups(user)
except Exception:
logger.exception(
'Exception running update_groups for services module %s on user %s',
svc,
user
)

View File

@@ -1,7 +1,7 @@
from copy import deepcopy
from unittest import mock
from django.test import TestCase
from django.test import override_settings, TestCase, TransactionTestCase
from django.contrib.auth.models import Group, Permission
from allianceauth.authentication.models import State
@@ -9,6 +9,9 @@ from allianceauth.eveonline.models import EveCharacter
from allianceauth.tests.auth_utils import AuthUtils
MODULE_PATH = 'allianceauth.services.signals'
class ServicesSignalsTestCase(TestCase):
def setUp(self):
self.member = AuthUtils.create_user('auth_member', disconnect_signals=True)
@@ -17,17 +20,12 @@ class ServicesSignalsTestCase(TestCase):
)
self.none_user = AuthUtils.create_user('none_user', disconnect_signals=True)
@mock.patch('allianceauth.services.signals.transaction')
@mock.patch('allianceauth.services.signals.ServicesHook')
def test_m2m_changed_user_groups(self, services_hook, transaction):
@mock.patch(MODULE_PATH + '.transaction', spec=True)
@mock.patch(MODULE_PATH + '.update_groups_for_user', spec=True)
def test_m2m_changed_user_groups(self, update_groups_for_user, transaction):
"""
Test that update_groups hook function is called on user groups change
"""
svc = mock.Mock()
svc.update_groups.return_value = None
svc.validate_user.return_value = None
services_hook.get_services.return_value = [svc]
# Overload transaction.on_commit so everything happens synchronously
transaction.on_commit = lambda fn: fn()
@@ -39,17 +37,11 @@ class ServicesSignalsTestCase(TestCase):
self.member.save()
# Assert
self.assertTrue(services_hook.get_services.called)
self.assertTrue(update_groups_for_user.delay.called)
args, _ = update_groups_for_user.delay.call_args
self.assertEqual(self.member.pk, args[0])
self.assertTrue(svc.update_groups.called)
args, kwargs = svc.update_groups.call_args
self.assertEqual(self.member, args[0])
self.assertTrue(svc.validate_user.called)
args, kwargs = svc.validate_user.call_args
self.assertEqual(self.member, args[0])
@mock.patch('allianceauth.services.signals.disable_user')
@mock.patch(MODULE_PATH + '.disable_user')
def test_pre_delete_user(self, disable_user):
"""
Test that disable_member is called when a user is deleted
@@ -60,7 +52,7 @@ class ServicesSignalsTestCase(TestCase):
args, kwargs = disable_user.call_args
self.assertEqual(self.none_user, args[0])
@mock.patch('allianceauth.services.signals.disable_user')
@mock.patch(MODULE_PATH + '.disable_user')
def test_pre_save_user_inactivation(self, disable_user):
"""
Test a user set inactive has disable_member called
@@ -72,7 +64,7 @@ class ServicesSignalsTestCase(TestCase):
args, kwargs = disable_user.call_args
self.assertEqual(self.member, args[0])
@mock.patch('allianceauth.services.signals.disable_user')
@mock.patch(MODULE_PATH + '.disable_user')
def test_disable_services_on_loss_of_main_character(self, disable_user):
"""
Test a user set inactive has disable_member called
@@ -84,8 +76,8 @@ class ServicesSignalsTestCase(TestCase):
args, kwargs = disable_user.call_args
self.assertEqual(self.member, args[0])
@mock.patch('allianceauth.services.signals.transaction')
@mock.patch('allianceauth.services.signals.ServicesHook')
@mock.patch(MODULE_PATH + '.transaction')
@mock.patch(MODULE_PATH + '.ServicesHook')
def test_m2m_changed_group_permissions(self, services_hook, transaction):
from django.contrib.contenttypes.models import ContentType
svc = mock.Mock()
@@ -116,8 +108,8 @@ class ServicesSignalsTestCase(TestCase):
args, kwargs = svc.validate_user.call_args
self.assertEqual(self.member, args[0])
@mock.patch('allianceauth.services.signals.transaction')
@mock.patch('allianceauth.services.signals.ServicesHook')
@mock.patch(MODULE_PATH + '.transaction')
@mock.patch(MODULE_PATH + '.ServicesHook')
def test_m2m_changed_user_permissions(self, services_hook, transaction):
from django.contrib.contenttypes.models import ContentType
svc = mock.Mock()
@@ -145,8 +137,8 @@ class ServicesSignalsTestCase(TestCase):
args, kwargs = svc.validate_user.call_args
self.assertEqual(self.member, args[0])
@mock.patch('allianceauth.services.signals.transaction')
@mock.patch('allianceauth.services.signals.ServicesHook')
@mock.patch(MODULE_PATH + '.transaction')
@mock.patch(MODULE_PATH + '.ServicesHook')
def test_m2m_changed_user_state_permissions(self, services_hook, transaction):
from django.contrib.contenttypes.models import ContentType
svc = mock.Mock()
@@ -180,7 +172,7 @@ class ServicesSignalsTestCase(TestCase):
args, kwargs = svc.validate_user.call_args
self.assertEqual(self.member, args[0])
@mock.patch('allianceauth.services.signals.ServicesHook')
@mock.patch(MODULE_PATH + '.ServicesHook')
def test_state_changed_services_validation_and_groups_update(self, services_hook):
"""Test a user changing state has service accounts validated and groups updated
"""
@@ -206,8 +198,7 @@ class ServicesSignalsTestCase(TestCase):
args, kwargs = svc.update_groups.call_args
self.assertEqual(self.member, args[0])
@mock.patch('allianceauth.services.signals.ServicesHook')
@mock.patch(MODULE_PATH + '.ServicesHook')
def test_state_changed_services_validation_and_groups_update_1(self, services_hook):
"""Test a user changing main has service accounts validated and sync updated
"""
@@ -238,7 +229,7 @@ class ServicesSignalsTestCase(TestCase):
args, kwargs = svc.sync_nickname.call_args
self.assertEqual(self.member, args[0])
@mock.patch('allianceauth.services.signals.ServicesHook')
@mock.patch(MODULE_PATH + '.ServicesHook')
def test_state_changed_services_validation_and_groups_update_2(self, services_hook):
"""Test a user changing main has service does not have accounts validated
and sync updated if the new main is equal to the old main
@@ -260,3 +251,71 @@ class ServicesSignalsTestCase(TestCase):
self.assertFalse(services_hook.get_services.called)
self.assertFalse(svc.validate_user.called)
self.assertFalse(svc.sync_nickname.called)
@mock.patch(
"allianceauth.services.modules.mumble.auth_hooks.MumbleService.update_groups"
)
@override_settings(CELERY_ALWAYS_EAGER=True, CELERY_EAGER_PROPAGATES_EXCEPTIONS=True)
class TestUserGroupBulkUpdate(TransactionTestCase):
def test_should_run_user_service_check_when_group_added_to_user(
self, mock_update_groups
):
# given
user = AuthUtils.create_user("Bruce Wayne")
AuthUtils.add_main_character_2(user, "Bruce Wayne", 1001)
group = Group.objects.create(name="Group")
mock_update_groups.reset_mock()
# when
user.groups.add(group)
# then
users_updated = {obj[0][0] for obj in mock_update_groups.call_args_list}
self.assertSetEqual(users_updated, {user})
def test_should_run_user_service_check_when_multiple_groups_are_added_to_user(
self, mock_update_groups
):
# given
user = AuthUtils.create_user("Bruce Wayne")
AuthUtils.add_main_character_2(user, "Bruce Wayne", 1001)
group_1 = Group.objects.create(name="Group 1")
group_2 = Group.objects.create(name="Group 2")
mock_update_groups.reset_mock()
# when
user.groups.add(group_1, group_2)
# then
users_updated = {obj[0][0] for obj in mock_update_groups.call_args_list}
self.assertSetEqual(users_updated, {user})
def test_should_run_user_service_check_when_user_added_to_group(
self, mock_update_groups
):
# given
user = AuthUtils.create_user("Bruce Wayne")
AuthUtils.add_main_character_2(user, "Bruce Wayne", 1001)
group = Group.objects.create(name="Group")
mock_update_groups.reset_mock()
# when
group.user_set.add(user)
# then
users_updated = {obj[0][0] for obj in mock_update_groups.call_args_list}
self.assertSetEqual(users_updated, {user})
def test_should_run_user_service_check_when_multiple_users_are_added_to_group(
self, mock_update_groups
):
# given
user_1 = AuthUtils.create_user("Bruce Wayne")
AuthUtils.add_main_character_2(user_1, "Bruce Wayne", 1001)
user_2 = AuthUtils.create_user("Peter Parker")
AuthUtils.add_main_character_2(user_2, "Peter Parker", 1002)
user_3 = AuthUtils.create_user("Lex Luthor")
AuthUtils.add_main_character_2(user_3, "Lex Luthor", 1011)
group = Group.objects.create(name="Group")
user_1.groups.add(group)
mock_update_groups.reset_mock()
# when
group.user_set.add(user_2, user_3)
# then
users_updated = {obj[0][0] for obj in mock_update_groups.call_args_list}
self.assertSetEqual(users_updated, {user_2, user_3})

View File

@@ -3,32 +3,50 @@ from unittest import mock
from celery_once import AlreadyQueued
from django.core.cache import cache
from django.test import TestCase
from django.test import override_settings, TestCase
from allianceauth.tests.auth_utils import AuthUtils
from allianceauth.services.tasks import validate_services
from allianceauth.services.tasks import validate_services, update_groups_for_user
from ..tasks import DjangoBackend
@override_settings(CELERY_ALWAYS_EAGER=True, CELERY_EAGER_PROPAGATES_EXCEPTIONS=True)
class ServicesTasksTestCase(TestCase):
def setUp(self):
self.member = AuthUtils.create_user('auth_member')
@mock.patch('allianceauth.services.tasks.ServicesHook')
def test_validate_services(self, services_hook):
# given
svc = mock.Mock()
svc.validate_user.return_value = None
services_hook.get_services.return_value = [svc]
# when
validate_services.delay(self.member.pk)
# then
self.assertTrue(services_hook.get_services.called)
self.assertTrue(svc.validate_user.called)
args, kwargs = svc.validate_user.call_args
args, _ = svc.validate_user.call_args
self.assertEqual(self.member, args[0]) # Assert correct user is passed to service hook function
@mock.patch('allianceauth.services.tasks.ServicesHook')
def test_update_groups_for_user(self, services_hook):
# given
svc = mock.Mock()
svc.validate_user.return_value = None
services_hook.get_services.return_value = [svc]
# when
update_groups_for_user.delay(self.member.pk)
# then
self.assertTrue(services_hook.get_services.called)
self.assertTrue(svc.validate_user.called)
args, _ = svc.validate_user.call_args
self.assertEqual(self.member, args[0]) # Assert correct user
self.assertTrue(svc.update_groups.called)
args, _ = svc.update_groups.call_args
self.assertEqual(self.member, args[0]) # Assert correct user
class TestDjangoBackend(TestCase):

View File

@@ -0,0 +1,3 @@
{% load static %}
<script src="{% static 'js/eve-time.js' %}"></script>

View File

@@ -0,0 +1,3 @@
{% load static %}
<script type="application/javascript" src="{% static 'js/filterDropDown/filterDropDown.min.js' %}">

View File

@@ -0,0 +1,3 @@
{% load static %}
<script src="{% static 'js/refresh_notifications.js' %}"></script>

View File

@@ -0,0 +1,3 @@
{% load static %}
<script src="{% static 'js/timers.js' %}"></script>

View File

@@ -38,6 +38,9 @@ logger = logging.getLogger(__name__)
@register.simple_tag()
def decimal_widthratio(this_value, max_value, max_width) -> str:
if max_value == 0:
return str(0)
return str(round(this_value/max_value * max_width, 2))

View File

View File

@@ -0,0 +1,21 @@
from redis import Redis
from django.core.cache import caches
try:
import django_redis
except ImportError:
django_redis = None
def get_redis_client() -> Redis:
"""Get the configured redis client used by Django for caching.
This function is a wrapper designed to work for both AA2 and AA3
and should always be used to ensure backwards compatibility.
"""
try:
return django_redis.get_redis_connection("default")
except AttributeError:
default_cache = caches["default"]
return default_cache.get_master_client()

View File

View File

@@ -0,0 +1,39 @@
from unittest.mock import MagicMock, patch
from django.test import TestCase
from allianceauth.utils.cache import get_redis_client
MODULE_PATH = "allianceauth.utils.cache"
class RedisClientStub:
"""Substitute for a Redis client."""
pass
class TestGetRedisClient(TestCase):
def test_should_work_with_aa2_api(self):
# given
mock_django_cache = MagicMock()
mock_django_cache.get_master_client.return_value = RedisClientStub()
# when
with patch(MODULE_PATH + ".django_redis", None), patch.dict(
MODULE_PATH + ".caches", {"default": mock_django_cache}
):
client = get_redis_client()
# then
self.assertIsInstance(client, RedisClientStub)
def test_should_work_with_aa3_api(self):
# given
mock_django_redis = MagicMock()
mock_django_redis.get_redis_connection.return_value = RedisClientStub()
# when
with patch(MODULE_PATH + ".django_redis", mock_django_redis), patch.dict(
MODULE_PATH + ".caches", {"default": None}
):
client = get_redis_client()
# then
self.assertIsInstance(client, RedisClientStub)

View File

@@ -1,7 +1,7 @@
PROTOCOL=https://
AUTH_SUBDOMAIN=%AUTH_SUBDOMAIN%
DOMAIN=%DOMAIN%
AA_DOCKER_TAG=registry.gitlab.com/allianceauth/allianceauth/auth:v2.11
AA_DOCKER_TAG=registry.gitlab.com/allianceauth/allianceauth/auth:v2.14.0
# Nginx Proxy Manager
PROXY_HTTP_PORT=80

View File

@@ -1,5 +1,5 @@
FROM python:3.9-slim
ARG AUTH_VERSION=2.11.1
ARG AUTH_VERSION=2.14.0
ARG AUTH_PACKAGE=allianceauth==${AUTH_VERSION}
ENV VIRTUAL_ENV=/opt/venv
ENV AUTH_USER=allianceauth

View File

@@ -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.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 `bash <(curl -s https://gitlab.com/allianceauth/allianceauth/-/raw/v2.14.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))

114
docs/_static/css/rtd_dark.css vendored Normal file
View File

@@ -0,0 +1,114 @@
/*!
* @name Readthedocs
* @namespace http://userstyles.org
* @description Styles the documentation pages hosted on Readthedocs.io
* @author Anthony Post
* @homepage https://userstyles.org/styles/142968
* @version 0.20170529055029
*
* Modified by Aloïs Dreyfus: 20200527-1037
* Modified by Erik Kalkoken: 20220615
*/
@media (prefers-color-scheme: dark) {
a:visited {
color: #bf84d8;
}
pre {
background-color: #2d2d2d !important;
}
.wy-nav-content {
background: #3c3c3c;
color: aliceblue;
}
.method dt, .class dt, .data dt, .attribute dt, .function dt,
.descclassname, .descname {
background-color: #525252 !important;
color: white !important;
}
.toc-backref {
color: grey !important;
}
code.literal {
background-color: #2d2d2d !important;
border: 1px solid #6d6d6d !important;
}
.wy-nav-content-wrap {
background-color: rgba(0, 0, 0, 0.6) !important;
}
.sidebar {
background-color: #191919 !important;
}
.sidebar-title {
background-color: #2b2b2b !important;
}
.xref, .py-meth {
color: #7ec3e6 !important;
}
.admonition, .note {
background-color: #2d2d2d !important;
}
.wy-side-nav-search {
background-color: inherit;
border-bottom: 1px solid #fcfcfc;
}
.wy-table thead, .rst-content table.docutils thead, .rst-content table.field-list thead {
background-color: #b9b9b9;
}
.wy-table thead th, .rst-content table.docutils thead th, .rst-content table.field-list thead th {
border: solid 2px #e1e4e5;
}
.wy-table thead p, .rst-content table.docutils thead p, .rst-content table.field-list thead p {
margin: 0;
}
.wy-table-odd td, .wy-table-striped tr:nth-child(2n-1) td, .rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td {
background-color: #343131;
}
.highlight .m {
color: inherit
}
/* Literal.Number */
.highlight .nv {
color: #3a7ca8
}
/* Name.Variable */
body {
text-align: justify;
}
.rst-content .section .admonition ul {
margin-bottom: 0;
}
li.toctree-l1 {
margin-top: 5px;
margin-bottom: 5px;
}
.wy-menu-vertical li code {
color: #E74C3C;
}
.wy-menu-vertical .xref {
color: #2980B9 !important;
}
}

View File

@@ -44,6 +44,8 @@ extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.napoleon',
'recommonmark',
'sphinxcontrib_django2',
'sphinx.ext.viewcode',
]
# Add any paths that contain templates here, relative to this directory.
@@ -60,7 +62,7 @@ master_doc = 'index'
# General information about the project.
project = 'Alliance Auth'
copyright = '2018-2020, Alliance Auth'
copyright = '2018-2022, Alliance Auth'
author = 'R4stl1n'
# The version info for the project you're documenting, acts as replacement for
@@ -111,6 +113,7 @@ html_theme_options = {
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
html_css_files = ["css/rtd_dark.css"]
# -- Options for HTMLHelp output ------------------------------------------

View File

@@ -150,12 +150,14 @@ sudo redis-server --daemonize yes
```eval_rst
.. note::
WSL does not have an init.d service, so it will not automatically start your services such as MySQL and Redis when you boot your Windows machine. For convenience we recommend putting the commands for starting these services in a bash script. Here is an example: ::
WSL does not have an init.d service, so it will not automatically start your services such as MySQL and Redis when you boot your Windows machine. For convenience we recommend putting the commands for starting these services in a bash script. Here is an example:
#/bin/bash
# start services for AA dev
sudo service mysql start
sudo redis-server --daemonize yes
::
#/bin/bash
# start services for AA dev
sudo service mysql start
sudo redis-server --daemonize yes
In addition it is possible to configure Windows to automatically start WSL services, but that procedure goes beyond the scopes of this guide.
```

View File

@@ -4,36 +4,4 @@ django-esi
The django-esi package provides an interface for easy access to the ESI.
Location: ``esi``
This is an external package. Please also see `here <https://gitlab.com/allianceauth/django-esi>`_ for it's official documentation.
clients
===========
.. automodule:: esi.clients
:members: esi_client_factory
:undoc-members:
decorators
===========
.. automodule:: esi.decorators
:members:
:undoc-members:
errors
===========
.. automodule:: esi.errors
:members:
:undoc-members:
models
===========
.. automodule:: esi.models
:members: Scope, Token
:exclude-members: objects, provider
:undoc-members:
This is an external package. Please see `here <https://django-esi.readthedocs.io/en/latest/>`_ for it's documentation.

View File

@@ -11,5 +11,4 @@ models
.. automodule:: allianceauth.eveonline.models
:members:
:exclude-members: objects, provider
:undoc-members:
:exclude-members: DoesNotExist, MultipleObjectsReturned

View File

@@ -11,4 +11,5 @@ To reduce redundancy and help speed up development we encourage developers to ut
eveonline
notifications
testutils
utils
```

View File

@@ -13,7 +13,8 @@ models
===========
.. autoclass:: allianceauth.notifications.models.Notification
:members: Level, mark_viewed, set_level
:members:
:exclude-members: DoesNotExist, MultipleObjectsReturned, save
managers
===========

View File

@@ -0,0 +1,14 @@
=============================
utils
=============================
Utilities and helper functions.
Location: ``allianceauth.utils``
cache
===========
.. automodule:: allianceauth.utils.cache
:members:
:undoc-members:

View File

@@ -1,15 +1,27 @@
=============
Template Tags
=============
=======================
Template tags & filters
=======================
The following template tags are available to be used by all apps. To use them just load the respeetive template tag in your template like so:
The following template tags and filters are available to be used by all apps. To use them just load them into your template like so:
.. code-block:: html
.. code-block:: html+django
{% load evelinks %}
Template Filters
================
evelinks
========
--------
Example for using an evelinks filter to render an alliance logo:
.. code-block:: html+django
<img src="{{ alliance_id|alliance_logo_url }}">
.. automodule:: allianceauth.eveonline.templatetags.evelinks
:members:

View File

@@ -2,13 +2,16 @@
sphinx>=3.2.1,<4.0.0
sphinx_rtd_theme==0.5.0
recommonmark==0.6.0
Jinja2<3.1
sphinxcontrib-django2
# Autodoc dependencies
django>=3.2,<4.0.0
django-celery-beat>=2.0.0
django-redis-cache
django-bootstrap-form
django-sortedm2m
django-esi>=3,<4
django-esi>=3,<5
celery>5,<6
celery_once
passlib

10
tox.ini
View File

@@ -1,7 +1,7 @@
[tox]
skipsdist = true
usedevelop = true
envlist = py{37,38,39,310,311}-{all,core}
envlist = py{37,38,39,310,311}-{all,core}, docs
[testenv]
setenv =
@@ -21,3 +21,11 @@ commands =
core: coverage run runtests.py allianceauth.authentication.tests.test_app_settings -v 2 --debug-mode
all: coverage report -m
all: coverage xml
[testenv:docs]
description = invoke sphinx-build to build the HTML docs
basepython = python3.9
deps = -r{toxinidir}/docs/requirements.txt
install_command =
commands =
sphinx-build -T -E -b html -d "{toxworkdir}/docs_doctree" -D language=en docs "{toxworkdir}/docs_out" {posargs}