Compare commits

..

13 Commits

Author SHA1 Message Date
Ariel Rin
2f320bc256 Version Bump 2.9.0a1 2021-05-09 06:45:17 +00:00
Ariel Rin
45b8d42b8e Merge branch 'javascript-modernization' into 'v2.9.x'
Cleanup and modernize JS

See merge request allianceauth/allianceauth!1305
2021-05-09 06:43:58 +00:00
Peter Pfeufer
bd2d19f867 Cleanup and modernize JS 2021-05-09 06:43:58 +00:00
Ariel Rin
0be404baca Merge branch 'py36remove' into 'v2.9.x'
Remove Python36 Testing and Support

See merge request allianceauth/allianceauth!1301
2021-05-09 06:42:56 +00:00
Ariel Rin
e6cee9ac83 Remove Python36 Testing and Support 2021-05-09 06:42:56 +00:00
Ariel Rin
d173a59441 Merge branch 'add-robots-txt' into 'v2.9.x'
Adding robots.txt to fix random 404 message

See merge request allianceauth/allianceauth!1302
2021-04-27 14:11:14 +00:00
Peter Pfeufer
8f59f2549a robots.txt added
This fixes the randomly appearing 404 error in Auth  (issue #1232)
2021-04-15 20:59:25 +02:00
Ariel Rin
630400fee4 Merge branch 'django-3-2-update' into 'v2.9.x'
Django 3.2 compatibility update

See merge request allianceauth/allianceauth!1300
2021-04-08 05:07:01 +00:00
Ariel Rin
1ec6929e91 Merge branch 'python39' into 'v2.9.x'
Add  Python3.9 Testing

See merge request allianceauth/allianceauth!1284
2021-04-08 04:49:15 +00:00
Ariel Rin
8c6bdd8ae2 Add Python3.9 Testing 2021-04-08 04:49:15 +00:00
Peter Pfeufer
e04138bced migrations 2021-04-07 18:40:39 +02:00
Peter Pfeufer
db5ad85811 add new default for PKs 2021-04-07 18:16:33 +02:00
Peter Pfeufer
5b44fd376d revert version cap on Django, so we get 3.2 again 2021-04-07 18:11:01 +02:00
71 changed files with 1137 additions and 1161 deletions

View File

@@ -7,37 +7,28 @@ include:
- template: Dependency-Scanning.gitlab-ci.yml
- template: Security/SAST.gitlab-ci.yml
before_script:
- apt-get update && apt-get install redis-server -y
- redis-server --daemonize yes
- python -V
- pip install wheel tox
sast:
stage: gitlab
before_script: []
dependency_scanning:
stage: gitlab
before_script:
- apt-get update && apt-get install redis-server libmariadbclient-dev -y
- redis-server --daemonize yes
- redis-cli ping
- python -V
- pip install wheel tox
test-3.6-core:
image: python:3.6-buster
script:
- tox -e py36-core
artifacts:
when: always
reports:
cobertura: coverage.xml
test-3.7-core:
image: python:3.7-buster
script:
- tox -e py37-core
before_script:
- apt-get update && apt-get install redis-server -y
- redis-server --daemonize yes
- redis-cli ping
- python -V
- pip install wheel tox
artifacts:
when: always
reports:
@@ -47,15 +38,27 @@ test-3.8-core:
image: python:3.8-buster
script:
- tox -e py38-core
before_script:
- apt-get update && apt-get install redis-server -y
- redis-server --daemonize yes
- redis-cli ping
- python -V
- pip install wheel tox
artifacts:
when: always
reports:
cobertura: coverage.xml
test-3.6-all:
image: python:3.6-buster
test-3.9-core:
image: python:3.9-buster
script:
- tox -e py36-all
- tox -e py39-core
before_script:
- apt-get update && apt-get install redis-server -y
- redis-server --daemonize yes
- redis-cli ping
- python -V
- pip install wheel tox
artifacts:
when: always
reports:
@@ -65,6 +68,12 @@ test-3.7-all:
image: python:3.7-buster
script:
- tox -e py37-all
before_script:
- apt-get update && apt-get install redis-server -y
- redis-server --daemonize yes
- redis-cli ping
- python -V
- pip install wheel tox
artifacts:
when: always
reports:
@@ -74,6 +83,27 @@ test-3.8-all:
image: python:3.8-buster
script:
- tox -e py38-all
before_script:
- apt-get update && apt-get install redis-server -y
- redis-server --daemonize yes
- redis-cli ping
- python -V
- pip install wheel tox
artifacts:
when: always
reports:
cobertura: coverage.xml
test-3.9-all:
image: python:3.9-buster
script:
- tox -e py39-all
before_script:
- apt-get update && apt-get install redis-server -y
- redis-server --daemonize yes
- redis-cli ping
- python -V
- pip install wheel tox
artifacts:
when: always
reports:
@@ -81,7 +111,7 @@ test-3.8-all:
deploy_production:
stage: deploy
image: python:3.8-buster
image: python:3.9-buster
before_script:
- pip install twine wheel

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.8.7'
__version__ = '2.9.0a1'
__title__ = 'Alliance Auth'
__url__ = 'https://gitlab.com/allianceauth/allianceauth'
NAME = '%s v%s' % (__title__, __version__)

View File

@@ -1,8 +1,10 @@
from django.conf import settings
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.models import User as BaseUser, \
Permission as BasePermission, Group
from django.db.models import Count, Q
from django.db.models import Q, F
from allianceauth.services.hooks import ServicesHook
from django.db.models.signals import pre_save, post_save, pre_delete, \
post_delete, m2m_changed
@@ -22,6 +24,11 @@ from allianceauth.eveonline.tasks import update_character
from .app_settings import AUTHENTICATION_ADMIN_USERS_MAX_GROUPS, \
AUTHENTICATION_ADMIN_USERS_MAX_CHARS
if 'allianceauth.eveonline.autogroups' in settings.INSTALLED_APPS:
_has_auto_groups = True
else:
_has_auto_groups = False
def make_service_hooks_update_groups_action(service):
"""
@@ -84,7 +91,8 @@ class UserProfileInline(admin.StackedInline):
if request.user.is_superuser:
query |= Q(userprofile__isnull=True)
else:
query |= Q(character_ownership__user=obj)
query |= Q(character_ownership__user=obj)
qs = EveCharacter.objects.filter(query)
formset = super().get_formset(request, obj=obj, **kwargs)
def get_kwargs(self, index):
@@ -113,8 +121,6 @@ def user_profile_pic(obj):
)
else:
return None
user_profile_pic.short_description = ''
@@ -146,7 +152,6 @@ def user_username(obj):
user_obj.username,
)
user_username.short_description = 'user / main'
user_username.admin_order_field = 'username'
@@ -163,8 +168,7 @@ def user_main_organization(obj):
else:
corporation = user_obj.profile.main_character.corporation_name
if user_obj.profile.main_character.alliance_id:
result = format_html(
'{}<br>{}',
result = format_html('{}<br>{}',
corporation,
user_obj.profile.main_character.alliance_name
)
@@ -172,7 +176,6 @@ def user_main_organization(obj):
result = corporation
return result
user_main_organization.short_description = 'Corporation / Alliance (Main)'
user_main_organization.admin_order_field = \
'profile__main_character__corporation_name'
@@ -202,13 +205,13 @@ class MainCorporationsFilter(admin.SimpleListFilter):
return qs.all()
else:
if qs.model == User:
return qs.filter(
profile__main_character__corporation_id=self.value()
)
return qs\
.filter(profile__main_character__corporation_id=\
self.value())
else:
return qs.filter(
user__profile__main_character__corporation_id=self.value()
)
return qs\
.filter(user__profile__main_character__corporation_id=\
self.value())
class MainAllianceFilter(admin.SimpleListFilter):
@@ -236,11 +239,12 @@ class MainAllianceFilter(admin.SimpleListFilter):
return qs.all()
else:
if qs.model == User:
return qs.filter(profile__main_character__alliance_id=self.value())
return qs\
.filter(profile__main_character__alliance_id=self.value())
else:
return qs.filter(
user__profile__main_character__alliance_id=self.value()
)
return qs\
.filter(user__profile__main_character__alliance_id=\
self.value())
def update_main_character_model(modeladmin, request, queryset):
@@ -255,7 +259,6 @@ def update_main_character_model(modeladmin, request, queryset):
'Update from ESI started for {} characters'.format(tasks_count)
)
update_main_character_model.short_description = \
'Update main character model from ESI'
@@ -264,16 +267,32 @@ class UserAdmin(BaseUserAdmin):
"""Extending Django's UserAdmin model
Behavior of groups and characters columns can be configured via settings
"""
class Media:
css = {
"all": ("authentication/css/admin.css",)
}
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.prefetch_related("character_ownerships__character", "groups")
class RealGroupsFilter(admin.SimpleListFilter):
"""Custom filter to get groups w/o Autogroups"""
title = 'group'
parameter_name = 'group_id__exact'
def lookups(self, request, model_admin):
qs = Group.objects.all().order_by(Lower('name'))
if _has_auto_groups:
qs = qs\
.filter(managedalliancegroup__isnull=True)\
.filter(managedcorpgroup__isnull=True)
return tuple([(x.pk, x.name) for x in qs])
def queryset(self, request, queryset):
if self.value() is None:
return queryset.all()
else:
return queryset.filter(groups__pk=self.value())
def get_actions(self, request):
actions = super(BaseUserAdmin, self).get_actions(request)
@@ -322,9 +341,11 @@ class UserAdmin(BaseUserAdmin):
return result
inlines = BaseUserAdmin.inlines + [UserProfileInline]
ordering = ('username', )
list_select_related = ('profile__state', 'profile__main_character')
show_full_result_count = True
ordering = ('username', )
list_select_related = True
show_full_result_count = True
list_display = (
user_profile_pic,
user_username,
@@ -337,9 +358,10 @@ class UserAdmin(BaseUserAdmin):
'_role'
)
list_display_links = None
list_filter = (
'profile__state',
'groups',
RealGroupsFilter,
MainCorporationsFilter,
MainAllianceFilter,
'is_active',
@@ -353,25 +375,41 @@ class UserAdmin(BaseUserAdmin):
)
def _characters(self, obj):
character_ownerships = list(obj.character_ownerships.all())
characters = [obj.character.character_name for obj in character_ownerships]
my_characters = [
x.character.character_name
for x in CharacterOwnership.objects\
.filter(user=obj)\
.order_by('character__character_name')\
.select_related()
]
return self._list_2_html_w_tooltips(
sorted(characters),
my_characters,
AUTHENTICATION_ADMIN_USERS_MAX_CHARS
)
_characters.short_description = 'characters'
def _state(self, obj):
return obj.profile.state.name
_state.short_description = 'state'
_state.admin_order_field = 'profile__state'
def _groups(self, obj):
my_groups = sorted([group.name for group in list(obj.groups.all())])
def _groups(self, obj):
if not _has_auto_groups:
my_groups = [x.name for x in obj.groups.order_by('name')]
else:
my_groups = [
x.name for x in obj.groups\
.filter(managedalliancegroup__isnull=True)\
.filter(managedcorpgroup__isnull=True)\
.order_by('name')
]
return self._list_2_html_w_tooltips(
my_groups, AUTHENTICATION_ADMIN_USERS_MAX_GROUPS
my_groups,
AUTHENTICATION_ADMIN_USERS_MAX_GROUPS
)
_groups.short_description = 'groups'
@@ -399,7 +437,7 @@ class UserAdmin(BaseUserAdmin):
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'))
kwargs["queryset"] = Group.objects.all().order_by(Lower('name'))
return super().formfield_for_manytomany(db_field, request, **kwargs)
@@ -408,14 +446,9 @@ class StateAdmin(admin.ModelAdmin):
list_select_related = True
list_display = ('name', 'priority', '_user_count')
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.annotate(user_count=Count("userprofile__id"))
def _user_count(self, obj):
return obj.user_count
return obj.userprofile_set.all().count()
_user_count.short_description = 'Users'
_user_count.admin_order_field = 'user_count'
fieldsets = (
(None, {
@@ -438,7 +471,7 @@ class StateAdmin(admin.ModelAdmin):
]
def formfield_for_manytomany(self, db_field, request, **kwargs):
"""overriding this formfield to have sorted lists in the form"""
"""overriding this formfield to have sorted lists in the form"""
if db_field.name == "member_characters":
kwargs["queryset"] = EveCharacter.objects.all()\
.order_by(Lower('character_name'))
@@ -448,10 +481,8 @@ class StateAdmin(admin.ModelAdmin):
elif db_field.name == "member_alliances":
kwargs["queryset"] = EveAllianceInfo.objects.all()\
.order_by(Lower('alliance_name'))
elif db_field.name == "permissions":
kwargs["queryset"] = Permission.objects.select_related("content_type").all()
return super().formfield_for_manytomany(db_field, request, **kwargs)
def has_delete_permission(self, request, obj=None):
if obj == get_guest_state():
return False
@@ -473,8 +504,7 @@ class BaseOwnershipAdmin(admin.ModelAdmin):
"all": ("authentication/css/admin.css",)
}
list_select_related = (
'user__profile__state', 'user__profile__main_character', 'character')
list_select_related = True
list_display = (
user_profile_pic,
user_username,
@@ -512,7 +542,6 @@ class CharacterOwnershipAdmin(BaseOwnershipAdmin):
class PermissionAdmin(admin.ModelAdmin):
actions = None
readonly_fields = [field.name for field in BasePermission._meta.fields]
search_fields = ('codename', )
list_display = ('admin_name', 'name', 'codename', 'content_type')
list_filter = ('content_type__app_label',)

View File

@@ -0,0 +1,33 @@
# Generated by Django 3.2 on 2021-04-07 16:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('authentication', '0017_remove_fleetup_permission'),
]
operations = [
migrations.AlterField(
model_name='characterownership',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='ownershiprecord',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='state',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='userprofile',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
]

View File

@@ -10,10 +10,9 @@ def get_admin_change_view_url(obj: object) -> str:
args=(obj.pk,)
)
def get_admin_search_url(ModelClass: type) -> str:
"""returns URL to search URL for model of given object"""
return '{}{}/'.format(
reverse('admin:app_list', args=(ModelClass._meta.app_label,)),
ModelClass.__name__.lower()
)
)

View File

@@ -1,8 +1,10 @@
from urllib.parse import quote
from unittest.mock import patch, MagicMock
from django.conf import settings
from django.contrib import admin
from django.contrib.admin.sites import AdminSite
from django.contrib.auth.models import Group
from django.contrib.auth.models import User as BaseUser, Group
from django.test import TestCase, RequestFactory, Client
from allianceauth.authentication.models import (
@@ -16,7 +18,8 @@ from allianceauth.tests.auth_utils import AuthUtils
from ..admin import (
BaseUserAdmin,
CharacterOwnershipAdmin,
CharacterOwnershipAdmin,
PermissionAdmin,
StateAdmin,
MainCorporationsFilter,
MainAllianceFilter,
@@ -32,6 +35,11 @@ from ..admin import (
)
from . import get_admin_change_view_url, get_admin_search_url
if 'allianceauth.eveonline.autogroups' in settings.INSTALLED_APPS:
_has_auto_groups = True
from allianceauth.eveonline.autogroups.models import AutogroupsConfig
else:
_has_auto_groups = False
MODULE_PATH = 'allianceauth.authentication.admin'
@@ -40,7 +48,6 @@ class MockRequest(object):
def __init__(self, user=None):
self.user = user
class TestCaseWithTestData(TestCase):
@classmethod
@@ -272,7 +279,6 @@ class TestStateAdmin(TestCaseWithTestData):
expected = 200
self.assertEqual(response.status_code, expected)
class TestUserAdmin(TestCaseWithTestData):
def setUp(self):
@@ -281,12 +287,24 @@ class TestUserAdmin(TestCaseWithTestData):
model=User, admin_site=AdminSite()
)
self.character_1 = self.user_1.character_ownerships.first().character
def _create_autogroups(self):
"""create autogroups for corps and alliances"""
if _has_auto_groups:
autogroups_config = AutogroupsConfig(
corp_groups = True,
alliance_groups = True
)
autogroups_config.save()
for state in State.objects.all():
autogroups_config.states.add(state)
autogroups_config.update_corp_group_membership(self.user_1)
# column rendering
def test_user_profile_pic_u1(self):
expected = (
'<img src="https://images.evetech.net/characters/1001/'
'portrait?size=32" class="img-circle">'
)
expected = ('<img src="https://images.evetech.net/characters/1001/'
'portrait?size=32" class="img-circle">')
self.assertEqual(user_profile_pic(self.user_1), expected)
def test_user_profile_pic_u3(self):
@@ -333,17 +351,37 @@ class TestUserAdmin(TestCaseWithTestData):
result = self.modeladmin._characters(self.user_3)
self.assertEqual(result, expected)
def test_groups_u1(self):
def test_groups_u1(self):
self._create_autogroups()
expected = 'Group 1'
result = self.modeladmin._groups(self.user_1)
self.assertEqual(result, expected)
def test_groups_u2(self):
def test_groups_u2(self):
self._create_autogroups()
expected = 'Group 2'
result = self.modeladmin._groups(self.user_2)
self.assertEqual(result, expected)
def test_groups_u3(self):
def test_groups_u3(self):
self._create_autogroups()
result = self.modeladmin._groups(self.user_3)
self.assertIsNone(result)
@patch(MODULE_PATH + '._has_auto_groups', False)
def test_groups_u1_no_autogroups(self):
expected = 'Group 1'
result = self.modeladmin._groups(self.user_1)
self.assertEqual(result, expected)
@patch(MODULE_PATH + '._has_auto_groups', False)
def test_groups_u2_no_autogroups(self):
expected = 'Group 2'
result = self.modeladmin._groups(self.user_2)
self.assertEqual(result, expected)
@patch(MODULE_PATH + '._has_auto_groups', False)
def test_groups_u3_no_autogroups(self):
result = self.modeladmin._groups(self.user_3)
self.assertIsNone(result)
@@ -375,10 +413,8 @@ class TestUserAdmin(TestCaseWithTestData):
def test_list_2_html_w_tooltips_w_cutoff(self):
items = ['one', 'two', 'three']
expected = (
'<span data-tooltip="one, two, three" '
'class="tooltip">one, two, (...)</span>'
)
expected = ('<span data-tooltip="one, two, three" '
'class="tooltip">one, two, (...)</span>')
result = self.modeladmin._list_2_html_w_tooltips(items, 2)
self.assertEqual(expected, result)
@@ -403,7 +439,63 @@ class TestUserAdmin(TestCaseWithTestData):
self.assertTrue(mock_message_user.called)
# filters
def test_filter_real_groups_with_autogroups(self):
class UserAdminTest(BaseUserAdmin):
list_filter = (UserAdmin.RealGroupsFilter,)
self._create_autogroups()
my_modeladmin = UserAdminTest(User, AdminSite())
# Make sure the lookups are correct
request = self.factory.get('/')
request.user = self.user_1
changelist = my_modeladmin.get_changelist_instance(request)
filters = changelist.get_filters(request)
filterspec = filters[0][0]
expected = [
(self.group_1.pk, self.group_1.name),
(self.group_2.pk, self.group_2.name),
]
self.assertEqual(filterspec.lookup_choices, expected)
# Make sure the correct queryset is returned
request = self.factory.get('/', {'group_id__exact': self.group_1.pk})
request.user = self.user_1
changelist = my_modeladmin.get_changelist_instance(request)
queryset = changelist.get_queryset(request)
expected = User.objects.filter(groups__in=[self.group_1])
self.assertSetEqual(set(queryset), set(expected))
@patch(MODULE_PATH + '._has_auto_groups', False)
def test_filter_real_groups_no_autogroups(self):
class UserAdminTest(BaseUserAdmin):
list_filter = (UserAdmin.RealGroupsFilter,)
my_modeladmin = UserAdminTest(User, AdminSite())
# Make sure the lookups are correct
request = self.factory.get('/')
request.user = self.user_1
changelist = my_modeladmin.get_changelist_instance(request)
filters = changelist.get_filters(request)
filterspec = filters[0][0]
expected = [
(self.group_1.pk, self.group_1.name),
(self.group_2.pk, self.group_2.name),
]
self.assertEqual(filterspec.lookup_choices, expected)
# Make sure the correct queryset is returned
request = self.factory.get('/', {'group_id__exact': self.group_1.pk})
request.user = self.user_1
changelist = my_modeladmin.get_changelist_instance(request)
queryset = changelist.get_queryset(request)
expected = User.objects.filter(groups__in=[self.group_1])
self.assertSetEqual(set(queryset), set(expected))
def test_filter_main_corporations(self):
class UserAdminTest(BaseUserAdmin):
@@ -511,6 +603,7 @@ class TestMakeServicesHooksActions(TestCaseWithTestData):
def sync_nicknames_bulk(self, user):
pass
def test_service_has_update_groups_only(self):
service = self.MyServicesHookTypeA()
mock_service = MagicMock(spec=service)

View File

@@ -1,10 +1,10 @@
from math import ceil
from unittest.mock import patch
from requests import RequestException
import requests_mock
from packaging.version import Version as Pep440Version
from django.core.cache import cache
from django.test import TestCase
from allianceauth.templatetags.admin_status import (
@@ -12,7 +12,8 @@ from allianceauth.templatetags.admin_status import (
_fetch_list_from_gitlab,
_current_notifications,
_current_version_summary,
_fetch_notification_issues_from_gitlab,
_fetch_notification_issues_from_gitlab,
_fetch_tags_from_gitlab,
_latests_versions
)
@@ -102,51 +103,35 @@ class TestStatusOverviewTag(TestCase):
class TestNotifications(TestCase):
def setUp(self) -> None:
cache.clear()
@requests_mock.mock()
def test_fetch_notification_issues_from_gitlab(self, requests_mocker):
# given
url = (
'https://gitlab.com/api/v4/projects/allianceauth%2Fallianceauth/issues'
'?labels=announcement'
)
requests_mocker.get(url, json=GITHUB_NOTIFICATION_ISSUES)
# when
result = _fetch_notification_issues_from_gitlab()
# then
self.assertEqual(result, GITHUB_NOTIFICATION_ISSUES)
@patch(MODULE_PATH + '.admin_status.cache')
def test_current_notifications_normal(self, mock_cache):
# given
mock_cache.get_or_set.return_value = GITHUB_NOTIFICATION_ISSUES
# when
result = _current_notifications()
# then
self.assertEqual(result['notifications'], GITHUB_NOTIFICATION_ISSUES[:5])
@requests_mock.mock()
def test_current_notifications_failed(self, requests_mocker):
# given
url = (
'https://gitlab.com/api/v4/projects/allianceauth%2Fallianceauth/issues'
'?labels=announcement'
)
requests_mocker.get(url, status_code=404)
# when
@patch(MODULE_PATH + '.admin_status.cache')
def test_current_notifications_failed(self, mock_cache):
mock_cache.get_or_set.side_effect = RequestException
result = _current_notifications()
# then
self.assertEqual(result['notifications'], list())
@patch(MODULE_PATH + '.admin_status.cache')
def test_current_notifications_is_none(self, mock_cache):
# given
mock_cache.get_or_set.return_value = None
# when
result = _current_notifications()
# then
self.assertEqual(result['notifications'], list())
@@ -158,17 +143,12 @@ class TestCeleryQueueLength(TestCase):
class TestVersionTags(TestCase):
def setUp(self) -> None:
cache.clear()
@patch(MODULE_PATH + '.admin_status.__version__', TEST_VERSION)
@patch(MODULE_PATH + '.admin_status.cache')
def test_current_version_info_normal(self, mock_cache):
# given
def test_current_version_info_normal(self, mock_cache):
mock_cache.get_or_set.return_value = GITHUB_TAGS
# when
result = _current_version_summary()
# then
self.assertTrue(result['latest_major'])
self.assertTrue(result['latest_minor'])
self.assertTrue(result['latest_patch'])
@@ -178,41 +158,32 @@ class TestVersionTags(TestCase):
self.assertEqual(result['latest_beta_version'], '2.4.6a1')
@patch(MODULE_PATH + '.admin_status.__version__', TEST_VERSION)
@requests_mock.mock()
def test_current_version_info_failed(self, requests_mocker):
# given
url = (
'https://gitlab.com/api/v4/projects/allianceauth%2Fallianceauth'
'/repository/tags'
)
requests_mocker.get(url, status_code=500)
# when
@patch(MODULE_PATH + '.admin_status.cache')
def test_current_version_info_failed(self, mock_cache):
mock_cache.get_or_set.side_effect = RequestException
expected = {}
result = _current_version_summary()
# then
self.assertEqual(result, {})
self.assertEqual(result, expected)
@requests_mock.mock()
def test_fetch_tags_from_gitlab(self, requests_mocker):
# given
url = (
'https://gitlab.com/api/v4/projects/allianceauth%2Fallianceauth'
'/repository/tags'
)
requests_mocker.get(url, json=GITHUB_TAGS)
# when
result = _current_version_summary()
# then
self.assertTrue(result)
result = _fetch_tags_from_gitlab()
self.assertEqual(result, GITHUB_TAGS)
@patch(MODULE_PATH + '.admin_status.__version__', TEST_VERSION)
@patch(MODULE_PATH + '.admin_status.cache')
def test_current_version_info_return_no_data(self, mock_cache):
# given
mock_cache.get_or_set.return_value = None
# when
mock_cache.get_or_set.return_value = None
expected = {}
result = _current_version_summary()
# then
self.assertEqual(result, {})
self.assertEqual(result, expected)
class TestLatestsVersion(TestCase):

View File

@@ -0,0 +1,23 @@
# Generated by Django 3.2 on 2021-04-07 16:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('corputils', '0005_cleanup_permissions'),
]
operations = [
migrations.AlterField(
model_name='corpmember',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='corpstats',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
]

View File

@@ -96,62 +96,24 @@ class EveAllianceForm(EveEntityForm):
@admin.register(EveCorporationInfo)
class EveCorporationInfoAdmin(admin.ModelAdmin):
search_fields = ['corporation_name']
list_display = ('corporation_name', 'alliance')
list_select_related = ('alliance',)
list_filter = (('alliance', admin.RelatedOnlyFieldListFilter),)
ordering = ('corporation_name',)
def has_change_permission(self, request, obj=None):
return False
def get_form(self, request, obj=None, **kwargs):
if not obj or not obj.pk:
return EveCorporationForm
return super().get_form(request, obj=obj, **kwargs)
return super(EveCorporationInfoAdmin, self).get_form(request, obj=obj, **kwargs)
@admin.register(EveAllianceInfo)
class EveAllianceInfoAdmin(admin.ModelAdmin):
search_fields = ['alliance_name']
list_display = ('alliance_name',)
ordering = ('alliance_name',)
def has_change_permission(self, request, obj=None):
return False
def get_form(self, request, obj=None, **kwargs):
if not obj or not obj.pk:
return EveAllianceForm
return super().get_form(request, obj=obj, **kwargs)
return super(EveAllianceInfoAdmin, self).get_form(request, obj=obj, **kwargs)
@admin.register(EveCharacter)
class EveCharacterAdmin(admin.ModelAdmin):
search_fields = [
'character_name',
'corporation_name',
'alliance_name',
'character_ownership__user__username'
]
list_display = (
'character_name', 'corporation_name', 'alliance_name', 'user', 'main_character'
)
list_select_related = (
'character_ownership', 'character_ownership__user__profile__main_character'
)
list_filter = (
'corporation_name',
'alliance_name',
(
'character_ownership__user__profile__main_character',
admin.RelatedOnlyFieldListFilter
),
)
ordering = ('character_name', )
def has_change_permission(self, request, obj=None):
return False
search_fields = ['character_name', 'corporation_name', 'alliance_name', 'character_ownership__user__username']
list_display = ('character_name', 'corporation_name', 'alliance_name', 'user', 'main_character')
@staticmethod
def user(obj):

View File

@@ -0,0 +1,28 @@
# Generated by Django 3.2 on 2021-04-07 16:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('eve_autogroups', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='autogroupsconfig',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='managedalliancegroup',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='managedcorpgroup',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
]

View File

@@ -32,12 +32,10 @@ class EveCharacterManager(models.Manager):
def update_character(self, character_id):
return self.get(character_id=character_id).update_character()
def get_character_by_id(self, character_id: int):
"""Return character by character ID or None if not found."""
try:
return self.get(character_id=character_id)
except self.model.DoesNotExist:
return None
def get_character_by_id(self, char_id):
if self.filter(character_id=char_id).exists():
return self.get(character_id=char_id)
return None
class EveAllianceProviderManager:

View File

@@ -0,0 +1,28 @@
# Generated by Django 3.2 on 2021-04-07 16:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('eveonline', '0014_auto_20210105_1413'),
]
operations = [
migrations.AlterField(
model_name='eveallianceinfo',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='evecharacter',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='evecorporationinfo',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 3.2 on 2021-04-07 16:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fleetactivitytracking', '0006_auto_20180803_0430'),
]
operations = [
migrations.AlterField(
model_name='fat',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='fatlink',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
]

View File

@@ -42,6 +42,7 @@
</div>
{% endblock content %}
{% block extra_script %}
$(document).ready(function(){
$("[rel=tooltip]").tooltip();
$(document).ready(function () {
$("[rel=tooltip]").tooltip();
});
{% endblock extra_script %}

View File

@@ -44,6 +44,7 @@
</div>
{% endblock content %}
{% block extra_script %}
$(document).ready(function(){
$("[rel=tooltip]").tooltip();
$(document).ready(function () {
$("[rel=tooltip]").tooltip();
});
{% endblock extra_script %}

View File

@@ -1,5 +1,5 @@
from django.apps import apps
from django.contrib.auth.models import Permission
from django.conf import settings
from django.contrib import admin
from django.contrib.auth.models import Group as BaseGroup, User
from django.db.models import Count
@@ -10,8 +10,9 @@ from django.dispatch import receiver
from .models import AuthGroup
from .models import GroupRequest
from . import signals
if 'eve_autogroups' in apps.app_configs:
if 'allianceauth.eveonline.autogroups' in settings.INSTALLED_APPS:
_has_auto_groups = True
else:
_has_auto_groups = False
@@ -40,6 +41,9 @@ class AuthGroupInlineAdmin(admin.StackedInline):
kwargs["queryset"] = Group.objects.order_by(Lower('name'))
return super().formfield_for_manytomany(db_field, request, **kwargs)
def has_add_permission(self, request, obj=None):
return False
def has_delete_permission(self, request, obj=None):
return False
@@ -93,9 +97,9 @@ class HasLeaderFilter(admin.SimpleListFilter):
else:
return queryset
class GroupAdmin(admin.ModelAdmin):
ordering = ('name',)
class GroupAdmin(admin.ModelAdmin):
list_select_related = True
ordering = ('name', )
list_display = (
'name',
'_description',
@@ -114,12 +118,9 @@ class GroupAdmin(admin.ModelAdmin):
list_filter.append(HasLeaderFilter)
search_fields = ('name', 'authgroup__description')
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),
)
@@ -135,7 +136,7 @@ class GroupAdmin(admin.ModelAdmin):
_member_count.admin_order_field = 'member_count'
def has_leader(self, obj):
return obj.authgroup.group_leaders.exists() or obj.authgroup.group_leader_groups.exists()
return obj.authgroup.group_leaders.exists()
has_leader.boolean = True
@@ -165,18 +166,6 @@ class GroupAdmin(admin.ModelAdmin):
filter_horizontal = ('permissions',)
inlines = (AuthGroupInlineAdmin,)
def formfield_for_manytomany(self, db_field, request, **kwargs):
if db_field.name == "permissions":
kwargs["queryset"] = Permission.objects.select_related("content_type").all()
return super().formfield_for_manytomany(db_field, request, **kwargs)
def save_formset(self, request, form, formset, change):
for inline_form in formset:
ag_instance = inline_form.save(commit=False)
ag_instance.group = form.instance
ag_instance.save()
formset.save()
class Group(BaseGroup):
class Meta:
@@ -184,29 +173,13 @@ class Group(BaseGroup):
verbose_name = BaseGroup._meta.verbose_name
verbose_name_plural = BaseGroup._meta.verbose_name_plural
try:
admin.site.unregister(BaseGroup)
finally:
admin.site.register(Group, GroupAdmin)
@admin.register(GroupRequest)
class GroupRequestAdmin(admin.ModelAdmin):
search_fields = ('user__username', )
list_display = ('id', 'group', 'user', '_leave_request', 'status')
list_filter = (
('group', admin.RelatedOnlyFieldListFilter),
('user', admin.RelatedOnlyFieldListFilter),
'leave_request',
'status'
)
def _leave_request(self, obj) -> True:
return obj.leave_request
_leave_request.short_description = 'is leave request'
_leave_request.boolean = True
admin.site.register(GroupRequest)
@receiver(pre_save, sender=Group)

View File

@@ -5,6 +5,3 @@ class GroupManagementConfig(AppConfig):
name = 'allianceauth.groupmanagement'
label = 'groupmanagement'
verbose_name = 'Group Management'
def ready(self):
from . import signals # noqa: F401

View File

@@ -0,0 +1,23 @@
# Generated by Django 3.2 on 2021-04-07 16:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('groupmanagement', '0015_make_descriptions_great_again'),
]
operations = [
migrations.AlterField(
model_name='grouprequest',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='requestlog',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
]

View File

@@ -55,6 +55,7 @@ class RequestLog(models.Model):
return user.profile.main_character
class AuthGroup(models.Model):
"""
Extends Django Group model with a one-to-one field
@@ -106,8 +107,7 @@ class AuthGroup(models.Model):
help_text="States listed here will have the ability to join this group provided "
"they have the proper permissions.")
description = models.TextField(max_length=512, blank=True, help_text="Short description <i>(max. 512 characters)"
"</i> of the group shown to users.")
description = models.TextField(max_length=512, blank=True, help_text="Short description <i>(max. 512 characters)</i> of the group shown to users.")
def __str__(self):
return self.group.name

View File

@@ -83,7 +83,7 @@
{% block extra_script %}
$.fn.dataTable.moment = function(format, locale) {
var types = $.fn.dataTable.ext.type;
let types = $.fn.dataTable.ext.type;
// Add type detection
types.detect.unshift(function(d) {

View File

@@ -40,121 +40,120 @@
</li>
</ul>
<div class="panel panel-default panel-tabs-aa">
<div class="panel-body">
<div class="tab-content">
<div id="add" class="tab-pane active">
{% if acceptrequests %}
<div class="table-responsive">
<table class="table table-aa">
<thead>
<div class="tab-content">
<div id="add" class="tab-pane fade in active panel panel-default">
<div class="panel-body">
{% if acceptrequests %}
<div class="table-responsive">
<table class="table table-aa">
<thead>
<tr>
<th>{% trans "Character" %}</th>
<th>{% trans "Organization" %}</th>
<th>{% trans "Group" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for acceptrequest in acceptrequests %}
<tr>
<th>{% trans "Character" %}</th>
<th>{% trans "Organization" %}</th>
<th>{% trans "Group" %}</th>
<th></th>
<td>
<img src="{{ acceptrequest.main_char|character_portrait_url:32 }}" class="img-circle" style="margin-right: 1rem;">
{% if acceptrequest.main_char %}
<a href="{{ acceptrequest.main_char|evewho_character_url }}" target="_blank">
{{ acceptrequest.main_char.character_name }}
</a>
{% else %}
{{ acceptrequest.user.username }}
{% endif %}
</td>
<td>
{% if acceptrequest.main_char %}
<a href="{{ acceptrequest.main_char|dotlan_corporation_url }}" target="_blank">
{{ acceptrequest.main_char.corporation_name }}
</a><br>
{{ acceptrequest.main_char.alliance_name|default_if_none:"" }}
{% else %}
{% trans "(unknown)" %}
{% endif %}
</td>
<td>{{ acceptrequest.group.name }}</td>
<td class="text-right">
<a href="{% url 'groupmanagement:accept_request' acceptrequest.id %}" class="btn btn-success">
{% trans "Accept" %}
</a>
<a href="{% url 'groupmanagement:reject_request' acceptrequest.id %}" class="btn btn-danger">
{% trans "Reject" %}
</a>
</td>
</tr>
</thead>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="alert alert-warning text-center">{% trans "No group add requests." %}</div>
{% endif %}
</div>
</div>
<tbody>
{% for acceptrequest in acceptrequests %}
<tr>
<td>
<img src="{{ acceptrequest.main_char|character_portrait_url:32 }}" class="img-circle" style="margin-right: 1rem;">
{% if acceptrequest.main_char %}
<a href="{{ acceptrequest.main_char|evewho_character_url }}" target="_blank">
{{ acceptrequest.main_char.character_name }}
</a>
{% else %}
{{ acceptrequest.user.username }}
{% endif %}
</td>
<td>
{% if acceptrequest.main_char %}
<a href="{{ acceptrequest.main_char|dotlan_corporation_url }}" target="_blank">
{{ acceptrequest.main_char.corporation_name }}
</a><br>
{{ acceptrequest.main_char.alliance_name|default_if_none:"" }}
{% else %}
{% trans "(unknown)" %}
{% endif %}
</td>
<td>{{ acceptrequest.group.name }}</td>
<td class="text-right">
<a href="{% url 'groupmanagement:accept_request' acceptrequest.id %}" class="btn btn-success">
{% trans "Accept" %}
</a>
<div id="leave" class="tab-pane fade panel panel-default">
<div class="panel-body">
{% if leaverequests %}
<div class="table-responsive">
<table class="table table-aa">
<thead>
<tr>
<th>{% trans "Character" %}</th>
<th>{% trans "Organization" %}</th>
<th>{% trans "Group" %}</th>
<th></th>
</tr>
</thead>
<a href="{% url 'groupmanagement:reject_request' acceptrequest.id %}" class="btn btn-danger">
{% trans "Reject" %}
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="alert alert-warning text-center">{% trans "No group add requests." %}</div>
{% endif %}
</div>
<div id="leave" class="tab-pane">
{% if leaverequests %}
<div class="table-responsive">
<table class="table table-aa">
<thead>
<tbody>
{% for leaverequest in leaverequests %}
<tr>
<th>{% trans "Character" %}</th>
<th>{% trans "Organization" %}</th>
<th>{% trans "Group" %}</th>
<th></th>
<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 %}
{% trans "(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">
{% trans "Accept" %}
</a>
<a href="{% url 'groupmanagement:leave_reject_request' leaverequest.id %}" class="btn btn-danger">
{% trans "Reject" %}
</a>
</td>
</tr>
</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 %}
{% trans "(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">
{% trans "Accept" %}
</a>
<a href="{% url 'groupmanagement:leave_reject_request' leaverequest.id %}" class="btn btn-danger">
{% trans "Reject" %}
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="alert alert-warning text-center">{% trans "No group leave requests." %}</div>
{% endif %}
</div>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="alert alert-warning text-center">{% trans "No group leave requests." %}</div>
{% endif %}
</div>
</div>
</div>

View File

@@ -47,8 +47,7 @@ class TestGroupAdmin(TestCase):
# group 2 - no leader
cls.group_2 = Group.objects.create(name='Group 2')
cls.group_2.authgroup.description = 'Internal Group'
cls.group_2.authgroup.internal = True
cls.group_2.authgroup.group_leader_groups.add(cls.group_1)
cls.group_2.authgroup.internal = True
cls.group_2.authgroup.save()
# group 3 - has leader
@@ -238,14 +237,10 @@ class TestGroupAdmin(TestCase):
result = self.modeladmin._member_count(obj)
self.assertEqual(result, expected)
def test_has_leader_user(self):
def test_has_leader(self):
result = self.modeladmin.has_leader(self.group_1)
self.assertTrue(result)
def test_has_leader_group(self):
result = self.modeladmin.has_leader(self.group_2)
self.assertTrue(result)
def test_properties_1(self):
expected = ['Default']
result = self.modeladmin._properties(self.group_1)

View File

@@ -0,0 +1,43 @@
# Generated by Django 3.2 on 2021-04-07 16:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('hrapplications', '0007_auto_20200918_1412'),
]
operations = [
migrations.AlterField(
model_name='application',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='applicationchoice',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='applicationcomment',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='applicationform',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='applicationquestion',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='applicationresponse',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
]

View File

@@ -1,35 +1,4 @@
from django.contrib import admin
from .models import Notification
@admin.register(Notification)
class NotificationAdmin(admin.ModelAdmin):
list_display = ("timestamp", "_main", "_state", "title", "level", "viewed")
list_select_related = ("user", "user__profile__main_character", "user__profile__state")
list_filter = (
"level",
"timestamp",
"user__profile__state",
('user__profile__main_character', admin.RelatedOnlyFieldListFilter),
)
ordering = ("-timestamp", )
search_fields = ["user__username", "user__profile__main_character__character_name"]
def _main(self, obj):
try:
return obj.user.profile.main_character
except AttributeError:
return obj.user
_main.admin_order_field = "user__profile__main_character__character_name"
def _state(self, obj):
return obj.user.profile.state
_state.admin_order_field = "user__profile__state__name"
def has_change_permission(self, request, obj=None):
return False
def has_add_permission(self, request) -> bool:
return False
admin.site.register(Notification)

View File

@@ -12,20 +12,21 @@ class NotificationHandler(logging.Handler):
try:
perm = Permission.objects.get(codename="logging_notifications")
message = record.getMessage()
if record.exc_text:
message += "\n\n"
message = message + record.exc_text
users = User.objects.filter(
Q(groups__permissions=perm) | Q(user_permissions=perm) | Q(is_superuser=True)).distinct()
for user in users:
notify(
user,
"%s [%s:%s]" % (record.levelname, record.funcName, record.lineno),
level=str([item[0] for item in Notification.LEVEL_CHOICES if item[1] == record.levelname][0]),
message=message
)
except Permission.DoesNotExist:
return
message = record.getMessage()
if record.exc_text:
message += "\n\n"
message = message + record.exc_text
users = User.objects.filter(
Q(groups__permissions=perm) | Q(user_permissions=perm) | Q(is_superuser=True)).distinct()
for user in users:
notify(
user,
"%s [%s:%s]" % (record.levelname, record.funcName, record.lineno),
level=Notification.Level.from_old_name(record.levelname),
message=message
)
pass

View File

@@ -12,7 +12,7 @@ class NotificationQuerySet(models.QuerySet):
"""Custom QuerySet for Notification model"""
def update(self, *args, **kwargs):
"""Override update to ensure cache is invalidated on very call."""
# overriden update to ensure cache is invaidated on very call
super().update(*args, **kwargs)
user_pks = set(self.select_related("user").values_list('user__pk', flat=True))
for user_pk in user_pks:
@@ -43,8 +43,6 @@ class NotificationManager(models.Manager):
if not message:
message = title
if level not in self.model.Level:
level = self.model.Level.INFO
obj = self.create(user=user, title=title, message=message, level=level)
logger.info("Created notification %s", obj)
return obj

View File

@@ -1,4 +1,4 @@
# Generated by Django 3.1.12 on 2021-07-01 21:22
# Generated by Django 3.2 on 2021-04-07 16:38
from django.db import migrations, models
@@ -12,7 +12,7 @@ class Migration(migrations.Migration):
operations = [
migrations.AlterField(
model_name='notification',
name='level',
field=models.CharField(choices=[('danger', 'danger'), ('warning', 'warning'), ('info', 'info'), ('success', 'success')], default='info', max_length=10),
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
]

View File

@@ -2,7 +2,6 @@ import logging
from django.db import models
from django.contrib.auth.models import User
from django.utils.translation import gettext_lazy as _
from .managers import NotificationManager
@@ -14,43 +13,17 @@ class Notification(models.Model):
NOTIFICATIONS_MAX_PER_USER_DEFAULT = 50
NOTIFICATIONS_REFRESH_TIME_DEFAULT = 30
class Level(models.TextChoices):
"""A notification level."""
DANGER = 'danger', _('danger') #:
WARNING = 'warning', _('warning') #:
INFO = 'info', _('info') #:
SUCCESS = 'success', _('success') #:
@classmethod
def from_old_name(cls, name: str) -> object:
"""Map old name to enum.
Raises ValueError for invalid names.
"""
name_map = {
"CRITICAL": cls.DANGER,
"ERROR": cls.DANGER,
"WARN": cls.WARNING,
"INFO": cls.INFO,
"DEBUG": cls.SUCCESS,
}
try:
return name_map[name]
except KeyError:
raise ValueError(f"Unknown name: {name}") from None
# LEVEL_CHOICES = (
# ('danger', 'CRITICAL'),
# ('danger', 'ERROR'),
# ('warning', 'WARN'),
# ('info', 'INFO'),
# ('success', 'DEBUG'),
# )
LEVEL_CHOICES = (
('danger', 'CRITICAL'),
('danger', 'ERROR'),
('warning', 'WARN'),
('info', 'INFO'),
('success', 'DEBUG'),
)
user = models.ForeignKey(User, on_delete=models.CASCADE)
level = models.CharField(choices=Level.choices, max_length=10, default=Level.INFO)
level = models.CharField(choices=LEVEL_CHOICES, max_length=10)
title = models.CharField(max_length=254)
message = models.TextField()
timestamp = models.DateTimeField(auto_now_add=True, db_index=True)
@@ -72,15 +45,22 @@ class Notification(models.Model):
Notification.objects.invalidate_user_notification_cache(self.user.pk)
def mark_viewed(self) -> None:
"""Mark notification as viewed."""
"""mark notification as viewed"""
logger.info("Marking notification as viewed: %s" % self)
self.viewed = True
self.save()
def set_level(self, level_name: str) -> None:
"""Set notification level according to old level name, e.g. 'CRITICAL'.
"""set notification level according to level name, e.g. 'CRITICAL'
Raises ValueError on invalid level names.
"""
self.level = self.Level.from_old_name(level_name)
raised exception on invalid level names
"""
try:
new_level = [
item[0] for item in self.LEVEL_CHOICES if item[1] == level_name
][0]
except IndexError:
raise ValueError('Invalid level name: %s' % level_name)
self.level = new_level
self.save()

View File

@@ -1,69 +0,0 @@
from logging import LogRecord, DEBUG
from django.contrib.auth.models import Permission, Group, User
from django.test import TestCase
from allianceauth.tests.auth_utils import AuthUtils
from ..handlers import NotificationHandler
from ..models import Notification
MODULE_PATH = 'allianceauth.notifications.handlers'
class TestHandler(TestCase):
def test_do_nothing_if_permission_does_not_exist(self):
# given
Permission.objects.get(codename="logging_notifications").delete()
handler = NotificationHandler()
record = LogRecord(
name="name",
level=DEBUG,
pathname="pathname",
lineno=42,
msg="msg",
args=[],
exc_info=None,
func="func"
)
# when
handler.emit(record)
# then
self.assertEqual(Notification.objects.count(), 0)
def test_should_emit_message_to_users_with_permission_only(self):
# given
AuthUtils.create_user('Lex Luthor')
user_permission = AuthUtils.create_user('Bruce Wayne')
user_permission = AuthUtils.add_permission_to_user_by_name(
"auth.logging_notifications", user_permission
)
group = Group.objects.create(name="Dummy Group")
perm = Permission.objects.get(codename="logging_notifications")
group.permissions.add(perm)
user_group = AuthUtils.create_user('Peter Parker')
user_group.groups.add(group)
user_superuser = User.objects.create_superuser("Clark Kent")
handler = NotificationHandler()
record = LogRecord(
name="name",
level=DEBUG,
pathname="pathname",
lineno=42,
msg="msg",
args=[],
exc_info=None,
func="func"
)
# when
handler.emit(record)
# then
self.assertEqual(Notification.objects.count(), 3)
users = set(Notification.objects.values_list("user__pk", flat=True))
self.assertSetEqual(
users, {user_permission.pk, user_group.pk, user_superuser.pk}
)
notif = Notification.objects.first()
self.assertEqual(notif.user, user_permission)
self.assertEqual(notif.title, "DEBUG [func:42]")
self.assertEqual(notif.level, "success")
self.assertEqual(notif.message, "msg")

View File

@@ -64,35 +64,6 @@ class TestUserNotify(TestCase):
self.assertEqual(obj.user, self.user)
self.assertEqual(obj.title, title)
self.assertEqual(obj.message, title)
def test_should_use_default_level_when_not_specified(self):
# given
title = 'dummy_title'
message = 'dummy message'
# when
Notification.objects.notify_user(self.user, title, message)
# then
self.assertEqual(Notification.objects.filter(user=self.user).count(), 1)
obj = Notification.objects.first()
self.assertEqual(obj.user, self.user)
self.assertEqual(obj.title, title)
self.assertEqual(obj.message, message)
self.assertEqual(obj.level, Notification.Level.INFO)
def test_should_use_default_level_when_invalid_level_given(self):
# given
title = 'dummy_title'
message = 'dummy message'
level = "invalid"
# when
Notification.objects.notify_user(self.user, title, message, level)
# then
self.assertEqual(Notification.objects.filter(user=self.user).count(), 1)
obj = Notification.objects.first()
self.assertEqual(obj.user, self.user)
self.assertEqual(obj.title, title)
self.assertEqual(obj.message, message)
self.assertEqual(obj.level, Notification.Level.INFO)
@override_settings(NOTIFICATIONS_MAX_PER_USER=3)
def test_remove_when_too_many_notifications(self):

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2 on 2021-04-07 16:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('optimer', '0004_on_delete'),
]
operations = [
migrations.AlterField(
model_name='optimer',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
]

View File

@@ -34,17 +34,15 @@
{% endblock %}
{% block extra_script %}
$('#id_start').datetimepicker({
setlocale: '{{ LANGUAGE_CODE }}',
{% if NIGHT_MODE %}
theme: 'dark',
{% else %}
theme: 'default',
{% endif %}
mask: true,
format: 'Y-m-d H:i',
minDate: 0
setlocale: '{{ LANGUAGE_CODE }}',
{% if NIGHT_MODE %}
theme: 'dark',
{% else %}
theme: 'default',
{% endif %}
mask: true,
format: 'Y-m-d H:i',
minDate: 0
});
{% endblock extra_script %}

View File

@@ -41,9 +41,10 @@
{% include 'bundles/moment-js.html' with locale=True %}
<script src="{% static 'js/timers.js' %}"></script>
<script type="application/javascript">
// Data
var timers = [
let timers = [
{% for op in optimer %}
{
'id': {{ op.id }},
@@ -52,67 +53,66 @@
},
{% endfor %}
];
</script>
<script type="application/javascript">
timedUpdate();
setAllLocalTimes();
// Start timed updates
setInterval(timedUpdate, 1000);
function timedUpdate() {
updateClock();
updateAllTimers();
}
function updateAllTimers () {
var l = timers.length;
for (var i=0; i < l; ++i) {
if (timers[i].expired) continue;
updateTimer(timers[i]);
}
}
/**
* Update a timer
* @param timer Timer information
* @param timer.start Date of the timer
* @param timer.id Id number of the timer
* @param timer.expired
*/
function updateTimer(timer) {
let updateTimer = function (timer) {
if (timer.start.isAfter(Date.now())) {
var duration = moment.duration(timer.start - moment(), 'milliseconds');
let duration = moment.duration(timer.start - moment(), 'milliseconds');
document.getElementById("countdown" + timer.id).innerHTML = getDurationString(duration);
} else {
timer.expired = true;
document.getElementById("countdown" + timer.id).innerHTML = "";
}
}
};
let updateAllTimers = function () {
let l = timers.length;
/**
* Set all local time fields
*/
function setAllLocalTimes() {
var l = timers.length;
for (var i=0; i < l; ++i) {
setLocalTime(timers[i]);
if (timers[i].expired) continue;
updateTimer(timers[i]);
}
}
};
/**
* Set the local time info for the timer
* @param timer Timer information
* @param timer.start Date of the timer
* @param timer.id Id number of the timer
*/
function setLocalTime(timer) {
let setLocalTime = function (timer) {
document.getElementById("localtime" + timer.id).innerHTML = timer.start.format("ddd @ LT");
}
};
function updateClock() {
/**
* Set all local time fields
*/
let setAllLocalTimes = function () {
let l = timers.length;
for (var i=0; i < l; ++i) {
setLocalTime(timers[i]);
}
};
let updateClock = function () {
document.getElementById("current-time").innerHTML = getCurrentEveTimeString();
}
};
let timedUpdate = function () {
updateClock();
updateAllTimers();
};
// Set initial values
setAllLocalTimes();
timedUpdate();
// Start timed updates
setInterval(timedUpdate, 1000);
</script>
{% endblock content %}

View File

@@ -24,7 +24,7 @@
</tr>
</thead>
<tbody>
{% for user in permission.users %}
{% for user in permission.users %}
{% include 'permissions_tool/audit_row.html' with type="User" name="Permission granted directlty" %}
{% endfor %}
{% for group in permission.groups %}
@@ -35,13 +35,13 @@
{% for state in permission.states %}
{% for profile in state.userprofile_set.all %}
{% with profile.user as user %}
{% include 'permissions_tool/audit_row.html' with type="State" name=state%}
{% include 'permissions_tool/audit_row.html' with type="State" name=state%}
{% endwith %}
{% endfor %}
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock content %}
@@ -56,15 +56,16 @@
{% block extra_script %}
$(document).ready(function() {
var groupColumn = 0;
var table = $('#tab_permissions_audit').DataTable({
let groupColumn = 0;
$('#tab_permissions_audit').DataTable({
columnDefs: [
{ "visible": false, "targets": groupColumn }
],
order: [[ groupColumn, 'asc' ], [ 2, 'asc' ] ],
filterDropDown:
{
columns: [
columns: [
{
idx: 0,
title: 'Source'
@@ -73,20 +74,20 @@
bootstrap: true
},
drawCallback: function ( settings ) {
var api = this.api();
var rows = api.rows( {page:'current'} ).nodes();
var last=null;
let api = this.api();
let rows = api.rows( {page:'current'} ).nodes();
let last = null;
api.column(groupColumn, {page:'current'} ).data().each( function ( group, i ) {
if ( last !== group ) {
$(rows).eq( i ).before(
'<tr class="tr-group"><td colspan="3">' + group + '</td></tr>'
);
last = group;
}
} );
}
} );
} );
{% endblock %}
{% endblock %}

View File

@@ -9,9 +9,9 @@
<div class="col-sm-12">
<h1 class="page-header">{% trans "Permissions Overview" %}</h1>
<p>
{% if request.GET.all != 'yes' %}
{% if request.GET.all != 'yes' %}
{% blocktrans %}Showing only applied permissions{% endblocktrans %}
<a href="{% url 'permissions_tool:overview' %}?all=yes" class="btn btn-primary">{% trans "Show All" %}</a>
<a href="{% url 'permissions_tool:overview' %}?all=yes" class="btn btn-primary">{% trans "Show All" %}</a>
{% else %}
{% blocktrans %}Showing all permissions{% endblocktrans %}
<a href="{% url 'permissions_tool:overview' %}?all=no" class="btn btn-primary">{% trans "Show Applied" %}</a>
@@ -79,7 +79,7 @@
{% endblock content %}
{% block extra_javascript %}
{% include 'bundles/datatables-js.html' %}
{% include 'bundles/datatables-js.html' %}
<script type="application/javascript" src="{% static 'js/filterDropDown/filterDropDown.min.js' %}"></script>
{% endblock %}
@@ -89,15 +89,16 @@
{% block extra_script %}
$(document).ready(function() {
var groupColumn = 0;
var table = $('#tab_permissions_overview').DataTable({
let groupColumn = 0;
$('#tab_permissions_overview').DataTable({
columnDefs: [
{ "visible": false, "targets": groupColumn }
],
order: [[ groupColumn, 'asc' ], [ 1, 'asc' ], [ 2, 'asc' ] ],
filterDropDown:
{
columns: [
columns: [
{
idx: 0
},
@@ -108,20 +109,20 @@
bootstrap: true
},
drawCallback: function ( settings ) {
var api = this.api();
var rows = api.rows( {page:'current'} ).nodes();
var last=null;
let api = this.api();
let rows = api.rows( {page:'current'} ).nodes();
let last = null;
api.column(groupColumn, {page:'current'} ).data().each( function ( group, i ) {
if ( last !== group ) {
$(rows).eq( i ).before(
'<tr class="tr-group"><td colspan="6">' + group + '</td></tr>'
);
last = group;
}
} );
}
} );
} );
{% endblock %}
{% endblock %}

View File

@@ -262,3 +262,5 @@ LOGGING = {
},
}
}
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2 on 2021-04-07 16:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('services', '0003_remove_broken_link'),
]
operations = [
migrations.AlterField(
model_name='nameformatconfig',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
]

View File

@@ -83,31 +83,10 @@ class DiscordUserManager(models.Manager):
if created is not False:
if created is None:
logger.debug(
"User %s with Discord ID %s is already a member. Forcing a Refresh",
"User %s with Discord ID %s is already a member.",
user,
user_id,
)
# Force an update cause the discord API won't do it for us.
if role_ids:
role_ids = list(role_ids)
updated = bot_client.modify_guild_member(
guild_id=DISCORD_GUILD_ID,
user_id=user_id,
role_ids=role_ids,
nick=nickname
)
if not updated:
# Could not update the new user so fail.
logger.warning(
"Failed to add user %s with Discord ID %s to Discord server",
user,
user_id,
)
return False
self.update_or_create(
user=user,
defaults={

View File

@@ -111,40 +111,6 @@ class TestAddUser(TestCase):
self.assertSetEqual(set(kwargs['role_ids']), {1, 2, 3})
self.assertIsNone(kwargs['nick'])
def test_can_activate_existing_user_with_roles_no_nick(
self,
mock_user_formatted_nick,
mock_user_group_names,
mock_exchange_auth_code_for_token,
mock_DiscordClient
):
roles = [
create_matched_role(ROLE_ALPHA),
create_matched_role(ROLE_BRAVO),
create_matched_role(ROLE_CHARLIE)
]
mock_user_formatted_nick.return_value = None
mock_user_group_names.return_value = ['a', 'b', 'c']
mock_exchange_auth_code_for_token.return_value = self.access_token
mock_DiscordClient.return_value.current_user.return_value = self.user_info
mock_DiscordClient.return_value.match_or_create_roles_from_names\
.return_value = roles
mock_DiscordClient.return_value.add_guild_member.return_value = None
mock_DiscordClient.return_value.modify_guild_member.return_value = True
result = DiscordUser.objects.add_user(self.user, authorization_code='abcdef')
self.assertTrue(result)
self.assertTrue(
DiscordUser.objects.filter(user=self.user, uid=TEST_USER_ID).exists()
)
self.assertTrue(mock_DiscordClient.return_value.add_guild_member.called)
self.assertTrue(mock_DiscordClient.return_value.modify_guild_member.called)
args, kwargs = mock_DiscordClient.return_value.modify_guild_member.call_args
self.assertEqual(kwargs['guild_id'], TEST_GUILD_ID)
self.assertEqual(kwargs['user_id'], TEST_USER_ID)
self.assertSetEqual(set(kwargs['role_ids']), {1, 2, 3})
self.assertIsNone(kwargs['nick'])
@patch(MODULE_PATH + '.managers.DISCORD_SYNC_NAMES', True)
def test_can_create_user_no_roles_with_nick(
self,
@@ -174,36 +140,6 @@ class TestAddUser(TestCase):
self.assertIsNone(kwargs['role_ids'])
self.assertEqual(kwargs['nick'], TEST_MAIN_NAME)
@patch(MODULE_PATH + '.managers.DISCORD_SYNC_NAMES', True)
def test_can_activate_existing_user_no_roles_with_nick(
self,
mock_user_formatted_nick,
mock_user_group_names,
mock_exchange_auth_code_for_token,
mock_DiscordClient
):
mock_user_formatted_nick.return_value = TEST_MAIN_NAME
mock_user_group_names.return_value = []
mock_exchange_auth_code_for_token.return_value = self.access_token
mock_DiscordClient.return_value.current_user.return_value = self.user_info
mock_DiscordClient.return_value.match_or_create_roles_from_names\
.return_value = []
mock_DiscordClient.return_value.add_guild_member.return_value = None
mock_DiscordClient.return_value.modify_guild_member.return_value = True
result = DiscordUser.objects.add_user(self.user, authorization_code='abcdef')
self.assertTrue(result)
self.assertTrue(
DiscordUser.objects.filter(user=self.user, uid=TEST_USER_ID).exists()
)
self.assertTrue(mock_DiscordClient.return_value.add_guild_member.called)
self.assertTrue(mock_DiscordClient.return_value.modify_guild_member.called)
args, kwargs = mock_DiscordClient.return_value.modify_guild_member.call_args
self.assertEqual(kwargs['guild_id'], TEST_GUILD_ID)
self.assertEqual(kwargs['user_id'], TEST_USER_ID)
self.assertIsNone(kwargs['role_ids'])
self.assertEqual(kwargs['nick'], TEST_MAIN_NAME)
@patch(MODULE_PATH + '.managers.DISCORD_SYNC_NAMES', False)
def test_can_create_user_no_roles_and_without_nick_if_turned_off(
self,
@@ -247,7 +183,6 @@ class TestAddUser(TestCase):
mock_DiscordClient.return_value.match_or_create_roles_from_names\
.return_value = []
mock_DiscordClient.return_value.add_guild_member.return_value = None
mock_DiscordClient.return_value.modify_guild_member.return_value = True
result = DiscordUser.objects.add_user(self.user, authorization_code='abcdef')
self.assertTrue(result)
@@ -255,31 +190,6 @@ class TestAddUser(TestCase):
DiscordUser.objects.filter(user=self.user, uid=TEST_USER_ID).exists()
)
self.assertTrue(mock_DiscordClient.return_value.add_guild_member.called)
self.assertTrue(mock_DiscordClient.return_value.modify_guild_member.called)
def test_can_activate_existing_guild_member_failure(
self,
mock_user_formatted_nick,
mock_user_group_names,
mock_exchange_auth_code_for_token,
mock_DiscordClient
):
mock_user_formatted_nick.return_value = None
mock_user_group_names.return_value = []
mock_exchange_auth_code_for_token.return_value = self.access_token
mock_DiscordClient.return_value.current_user.return_value = self.user_info
mock_DiscordClient.return_value.match_or_create_roles_from_names\
.return_value = []
mock_DiscordClient.return_value.add_guild_member.return_value = None
mock_DiscordClient.return_value.modify_guild_member.return_value = False
result = DiscordUser.objects.add_user(self.user, authorization_code='abcdef')
self.assertFalse(result)
self.assertFalse(
DiscordUser.objects.filter(user=self.user, uid=TEST_USER_ID).exists()
)
self.assertTrue(mock_DiscordClient.return_value.add_guild_member.called)
self.assertTrue(mock_DiscordClient.return_value.modify_guild_member.called)
def test_return_false_when_user_creation_fails(
self,

View File

@@ -12,20 +12,12 @@ import base64
import hmac
import hashlib
try:
from urllib import unquote, urlencode
except ImportError: #py3
from urllib.parse import unquote, urlencode
try:
from urlparse import parse_qs
except ImportError: #py3
from urllib.parse import parse_qs
from urllib.parse import unquote, urlencode, parse_qs
import logging
logger = logging.getLogger(__name__)
ACCESS_PERM = 'discourse.access_discourse'
@@ -55,7 +47,7 @@ def discourse_sso(request):
# Validate the payload
try:
payload = unquote(payload).encode('utf-8')
decoded = base64.decodestring(payload).decode('utf-8')
decoded = base64.decodebytes(payload).decode('utf-8')
assert 'nonce' in decoded
assert len(payload) > 0
except AssertionError:
@@ -86,7 +78,7 @@ def discourse_sso(request):
if main_char:
params['avatar_url'] = main_char.portrait_url(256)
return_payload = base64.encodestring(urlencode(params).encode('utf-8'))
return_payload = base64.encodebytes(urlencode(params).encode('utf-8'))
h = hmac.new(key, return_payload, digestmod=hashlib.sha256)
query_string = urlencode({'sso': return_payload, 'sig': h.hexdigest()})

View File

@@ -1,33 +0,0 @@
# Generated by Django 3.1.6 on 2021-03-23 13:10
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mumble', '0001_squashed_0011_auto_20201011_1009'),
]
operations = [
migrations.AddField(
model_name='mumbleuser',
name='last_connect',
field=models.DateTimeField(blank=True, editable=False, help_text='Timestamp of the users Last Connection to Mumble', max_length=254, null=True, verbose_name='Last Connection'),
),
migrations.AddField(
model_name='mumbleuser',
name='last_disconnect',
field=models.DateTimeField(blank=True, editable=False, help_text='Timestamp of the users Last Disconnection to Mumble', max_length=254, null=True, verbose_name='Last Disconnection'),
),
migrations.AddField(
model_name='mumbleuser',
name='release',
field=models.TextField(blank=True, editable=False, help_text='The Mumble Release the user last authenticated with', max_length=254, null=True, verbose_name='Mumble Release'),
),
migrations.AddField(
model_name='mumbleuser',
name='version',
field=models.IntegerField(blank=True, editable=False, help_text='Client version. Major version in upper 16 bits, followed by 8 bits of minor version and 8 bits of patchlevel. Version 1.2.3 = 0x010203.', null=True, verbose_name='Mumble Version'),
),
]

View File

@@ -74,41 +74,7 @@ class MumbleUser(AbstractServiceModel):
editable=False,
help_text="Hash of Mumble client certificate as presented when user authenticates"
)
display_name = models.CharField(
max_length=254,
unique=True
)
release = models.TextField(
verbose_name="Mumble Release",
max_length=254,
blank=True,
null=True,
editable=False,
help_text="The Mumble Release the user last authenticated with"
)
version = models.IntegerField(
verbose_name="Mumble Version",
blank=True,
null=True,
editable=False,
help_text="Client version. Major version in upper 16 bits, followed by 8 bits of minor version and 8 bits of patchlevel. Version 1.2.3 = 0x010203."
)
last_connect = models.DateTimeField(
verbose_name="Last Connection",
max_length=254,
blank=True,
null=True,
editable=False,
help_text="Timestamp of the users Last Connection to Mumble"
)
last_disconnect = models.DateTimeField(
verbose_name="Last Disconnection",
max_length=254,
blank=True,
null=True,
editable=False,
help_text="Timestamp of the users Last Disconnection to Mumble"
)
display_name = models.CharField(max_length=254, unique=True)
objects = MumbleManager()

View File

@@ -15,7 +15,6 @@ class Teamspeak3UserAdmin(ServicesUserAdmin):
@admin.register(AuthTS)
class AuthTSgroupAdmin(admin.ModelAdmin):
change_list_template = 'admin/teamspeak3/authts/change_list.html'
ordering = ('auth_group__name', )
list_select_related = True
@@ -29,7 +28,7 @@ class AuthTSgroupAdmin(admin.ModelAdmin):
return [x for x in obj.ts_group.all().order_by('ts_group_id')]
_ts_group.short_description = 'ts groups'
# _ts_group.admin_order_field = 'profile__state'
#_ts_group.admin_order_field = 'profile__state'
@admin.register(StateGroup)

View File

@@ -0,0 +1,28 @@
# Generated by Django 3.2 on 2021-04-07 16:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('teamspeak3', '0005_stategroup'),
]
operations = [
migrations.AlterField(
model_name='authts',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='stategroup',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='usertsgroup',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
]

View File

@@ -1,11 +0,0 @@
{% extends "admin/change_list.html" %}
{% load i18n static %}
{% block object-tools-items %}
{{ block.super }}
<li>
<a href="{% url 'teamspeak3:admin_update_ts3_groups' %}" class="btn btn-high">
Update TS3 groups
</a>
</li>
{% endblock %}

View File

@@ -216,24 +216,6 @@ class Teamspeak3ViewsTestCase(TestCase):
self.assertEqual(ts3_user.perm_key, '123abc')
self.assertTrue(tasks_manager.return_value.__enter__.return_value.update_groups.called)
@mock.patch(MODULE_PATH + '.views.Teamspeak3Tasks')
@mock.patch(MODULE_PATH + '.views.messages')
def test_should_update_ts_groups(self, messages, Teamspeak3Tasks):
# given
self.member.is_superuser = True
self.member.is_staff = True
self.member.save()
self.login()
# when
response = self.client.get(urls.reverse('teamspeak3:admin_update_ts3_groups'))
# then
self.assertRedirects(
response, urls.reverse('admin:teamspeak3_authts_changelist'),
target_status_code=200
)
self.assertTrue(messages.info.called)
self.assertTrue(Teamspeak3Tasks.run_ts3_group_update.delay.called)
class Teamspeak3SignalsTestCase(TestCase):
def setUp(self):

View File

@@ -6,14 +6,12 @@ app_name = 'teamspeak3'
module_urls = [
# Teamspeak3 service control
url(r'^activate/$', views.activate_teamspeak3, name='activate'),
url(r'^deactivate/$', views.deactivate_teamspeak3, name='deactivate'),
url(r'^reset_perm/$', views.reset_teamspeak3_perm, name='reset_perm'),
url(
r'^admin_update_ts3_groups/$',
views.admin_update_ts3_groups,
name='admin_update_ts3_groups'
),
url(r'^activate/$', views.activate_teamspeak3,
name='activate'),
url(r'^deactivate/$', views.deactivate_teamspeak3,
name='deactivate'),
url(r'^reset_perm/$', views.reset_teamspeak3_perm,
name='reset_perm'),
# Teamspeak Urls
url(r'^verify/$', views.verify_teamspeak3, name='verify'),

View File

@@ -1,7 +1,6 @@
import logging
from django.contrib import messages
from django.contrib.admin.views.decorators import staff_member_required
from django.contrib.auth.decorators import login_required, permission_required
from django.shortcuts import render, redirect
from django.conf import settings
@@ -100,11 +99,3 @@ def reset_teamspeak3_perm(request):
logger.error("Unsuccessful attempt to reset TS3 permission key for user %s" % request.user)
messages.error(request, _('An error occurred while processing your TeamSpeak3 account.'))
return redirect("services:services")
@login_required
@staff_member_required
def admin_update_ts3_groups(request):
Teamspeak3Tasks.run_ts3_group_update.delay()
messages.info(request, "Started updating TS3 server groups...")
return redirect("admin:teamspeak3_authts_changelist")

View File

@@ -0,0 +1,23 @@
# Generated by Django 3.2 on 2021-04-07 16:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('srp', '0004_on_delete'),
]
operations = [
migrations.AlterField(
model_name='srpfleetmain',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='srpuserrequest',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
]

View File

@@ -43,17 +43,15 @@
{% endblock %}
{% block extra_script %}
$('#id_fleet_time').datetimepicker({
setlocale: '{{ LANGUAGE_CODE }}',
{% if NIGHT_MODE %}
theme: 'dark',
{% else %}
theme: 'default',
{% endif %}
mask: true,
format: 'Y-m-d H:i',
minDate: 0
setlocale: '{{ LANGUAGE_CODE }}',
{% if NIGHT_MODE %}
theme: 'dark',
{% else %}
theme: 'default',
{% endif %}
mask: true,
format: 'Y-m-d H:i',
minDate: 0
});
{% endblock extra_script %}

View File

@@ -187,7 +187,7 @@ ESC to cancel{% endblocktrans %}"id="blah"></i></th>
{% include 'bundles/x-editable-js.html' %}
{% include 'bundles/moment-js.html' %}
{% include 'bundles/clipboard-js.html' %}
<script>
var clipboard = new ClipboardJS('.copy-text-fa-icon');
clipboard.on('success', function (e) {
@@ -206,73 +206,71 @@ ESC to cancel{% endblocktrans %}"id="blah"></i></th>
{% endblock extra_javascript %}
{% block extra_script %}
$(document).ready(function() {
$.fn.editable.defaults.mode = 'inline';
$.fn.editable.defaults.showbuttons = false;
$.fn.editable.defaults.highlight = "#AAFF80";
$(document).ready(function() {
$.fn.editable.defaults.mode = 'inline';
$.fn.editable.defaults.showbuttons = false;
$.fn.editable.defaults.highlight = "#AAFF80";
$.fn.dataTable.moment = function(format, locale) {
let types = $.fn.dataTable.ext.type;
$('.srp').editable({
display: function(value, response) {
return false;
},
success: function(response, newValue) {
newValue = parseInt(newValue);
newvalue = newValue.toLocaleString() + " ISK";
$(this).html(newvalue.bold());
},
validate: function(value) {
if (value === null || value === '') {
return 'Empty values not allowed';
}
}
});
$('.srp').on('hidden', function(e, reason){
if(reason === 'save' || reason === 'nochange') {
var $next = $(this).closest('tr').next().find('.editable');
setTimeout(function() {
$next.editable('show');
}, 400);
}
});
});
// Add type detection
types.detect.unshift(function(d) {
return moment(d, format, locale, true).isValid() ?
'moment-' + format :
null;
});
$(document).ready(function(){
$("[rel=tooltip]").tooltip({ placement: 'top'});
});
// Add sorting method - use an integer for the sorting
types.order[ 'moment-' + format+'-pre' ] = function(d) {
return moment(d, format, locale, true).unix();
};
};
$.fn.dataTable.moment('YYYY-MMM-D, HH:mm');
$.fn.dataTable.moment = function(format, locale) {
var types = $.fn.dataTable.ext.type;
$('.srp').editable({
display: function(value, response) {
return false;
},
success: function(response, newValue) {
newValue = parseInt(newValue);
let newValueOutput = newValue.toLocaleString() + " ISK";
// Add type detection
types.detect.unshift(function(d) {
return moment(d, format, locale, true).isValid() ?
'moment-'+format :
null;
} );
$(this).html(newValueOutput.bold());
},
validate: function(value) {
if (value === null || value === '') {
return 'Empty values not allowed';
}
}
});
// Add sorting method - use an integer for the sorting
types.order[ 'moment-'+format+'-pre' ] = function(d) {
return moment(d, format, locale, true).unix();
};
};
$('.srp').on('hidden', function(e, reason){
if(reason === 'save' || reason === 'nochange') {
let $next = $(this).closest('tr').next().find('.editable');
$(document).ready( function(){
$.fn.dataTable.moment('YYYY-MMM-D, HH:mm');
setTimeout(function() {
$next.editable('show');
}, 400);
}
});
$('table.srplist').DataTable({
"order": [[ 6, "asc" ]],
"paging": false,
"columnDefs": [{
"targets": [1, 8],
"orderable": false
},
{
"targets": [4, 5],
"type": "num"
}]
$('table.srplist').DataTable({
"order": [[ 6, "asc" ]],
"paging": false,
"columnDefs": [
{
"targets": [1, 8],
"orderable": false
},
{
"targets": [4, 5],
"type": "num"
}
]
});
// tooltip
$("[rel=tooltip]").tooltip({ placement: 'top'});
});
});
{% endblock extra_script %}

View File

@@ -43,51 +43,27 @@ ul.list-group.list-group-horizontal > li.list-group-item {
justify-content: center;
}
@media all {
/* style nav tabs in dark mode*/
.template-dark-mode .nav-tabs > li.active > a {
background-color: rgb(70, 69, 69)!important;
color: rgb(255, 255, 255) !important;
}
/* style group headers within a table */
.tr-group {
font-weight: bold;
background-color: #e6e6e6 !important;
}
.panel-tabs-aa {
border-top: none;
border-top-left-radius: 0%;
border-top-right-radius: 0%;
}
/* style group headers within a table */
.template-light-mode .tr-group {
font-weight: bold;
background-color: #e6e6e6 !important;
}
.template-dark-mode .tr-group {
font-weight: bold;
background-color: rgb(105, 105, 105) !important;
}
/* default style for tables */
.template-light-mode .table-aa > thead > tr > th{
border-bottom: 1px solid #f2f2f2;
}
.template-dark-mode .table-aa > thead > tr > th{
border-bottom: 1px solid rgb(70, 69, 69);
}
.table-aa > thead > tr > th{
vertical-align: middle;
}
.template-light-mode .table-aa > tbody > tr > td{
border-bottom: 1px solid #f2f2f2;
}
.template-dark-mode .table-aa > tbody > tr > td{
border-bottom: 1px solid rgb(70, 69, 69);
}
.table-aa > tbody > tr > td {
vertical-align: middle;
}
.table-aa > tbody > tr:last-child {
border-bottom: none;
}
/* default style for tables */
.table-aa > thead > tr > th{
border-bottom: 1px solid #f2f2f2;
}
.table-aa > thead > tr > th{
vertical-align: middle;
}
.table-aa > tbody > tr > td{
border-bottom: 1px solid #f2f2f2;
}
.table-aa > tbody > tr > td {
vertical-align: middle;
}
.table-aa > tbody > tr:last-child {
border-bottom: none;
}
/* highlight active menu items

View File

@@ -1,3 +1,5 @@
/* global notificationUPdateSettings */
/*
This script refreshed the unread notification count in the top menu
on a regular basis so to keep the user apprised about newly arrived
@@ -6,70 +8,67 @@
The refresh rate can be changes via the Django setting NOTIFICATIONS_REFRESH_TIME.
See documentation for details.
*/
$(function () {
var elem = document.getElementById("dataExport");
var notificationsListViewUrl = elem.getAttribute("data-notificationsListViewUrl");
var notificationsRefreshTime = elem.getAttribute("data-notificationsRefreshTime");
var userNotificationsCountViewUrl = elem.getAttribute(
"data-userNotificationsCountViewUrl"
);
'use strict';
let notificationsListViewUrl = notificationUPdateSettings.notificationsListViewUrl;
let notificationsRefreshTime = notificationUPdateSettings.notificationsRefreshTime;
let userNotificationsCountViewUrl = notificationUPdateSettings.userNotificationsCountViewUrl;
// update the notification unread count in the top menu
function update_notifications() {
let updateNotifications = function () {
$.getJSON(userNotificationsCountViewUrl, function (data, status) {
if (status == 'success') {
var innerHtml = "";
var unread_count = data.unread_count;
if (unread_count > 0) {
if (status === 'success') {
let innerHtml = '';
let unreadCount = data.unread_count;
if (unreadCount > 0) {
innerHtml = (
`Notifications <span class="badge">${unread_count}</span>`
)
`Notifications <span class="badge">${unreadCount}</span>`
);
} else {
innerHtml = '<i class="far fa-bell"></i>';
}
else {
innerHtml = '<i class="far fa-bell"></i>'
}
$("#menu_item_notifications").html(
$('#menu_item_notifications').html(
`<a href="${notificationsListViewUrl}">${innerHtml}</a>`
);
}
else {
} else {
console.error(
`Failed to load HTMl to render notifications item. Error: `
`${xhr.status}': '${xhr.statusText}`
`Failed to load HTMl to render notifications item. Error: ${xhr.status}': '${xhr.statusText}`
);
}
});
}
};
var myInterval;
let myInterval;
// activate automatic refreshing every x seconds
function activate_refreshing() {
let activateRefreshing = function () {
if (notificationsRefreshTime > 0) {
myInterval = setInterval(
update_notifications, notificationsRefreshTime * 1000
updateNotifications, notificationsRefreshTime * 1000
);
}
}
};
// deactivate automatic refreshing
function deactivate_refreshing() {
let deactivateRefreshing = function () {
if ((notificationsRefreshTime > 0) && (typeof myInterval !== 'undefined')) {
clearInterval(myInterval)
clearInterval(myInterval);
}
}
};
// refreshing only happens on active browser tab
$(document).on({
'show': function () {
activate_refreshing()
activateRefreshing();
},
'hide': function () {
deactivate_refreshing()
deactivateRefreshing();
}
});
// Initial start of refreshing on script loading
activate_refreshing()
activateRefreshing();
});

View File

@@ -1,23 +1,49 @@
/* global moment */
/**
* Get a duration string like countdown.js
* e.g. "1y 2d 3h 4m 5s"
* @param duration moment.duration
*/
function getDurationString(duration) {
var out = "";
* Get a duration string like countdown.js
* e.g. "1y 2d 3h 4m 5s"
*
* @param duration
* @returns {string}
*/
let getDurationString = function (duration) {
'use strict';
let out = '';
if (duration.years()) {
out += duration.years() + 'y ';
}
if (duration.months()) {
out += duration.months() + 'm ';
}
if (duration.days()) {
out += duration.days() + 'd ';
}
return out + duration.hours() + "h " + duration.minutes() + "m " + duration.seconds() + "s";
}
return out + duration.hours() + 'h ' + duration.minutes() + 'm ' + duration.seconds() + 's';
};
function getCurrentEveTimeString() {
return moment().utc().format('dddd LL HH:mm:ss')
}
/**
* returns the current eve time as a formatted string
*
* condition:
* only if moment.js is loaded before,
* if not this function returns an empty string to avoid JS errors from happening.
*
* @returns {string}
*/
let getCurrentEveTimeString = function () {
'use strict';
let returnValue = '';
if (window.moment) {
returnValue = moment().utc().format('dddd LL HH:mm:ss');
}
return returnValue;
};

View File

@@ -0,0 +1,2 @@
User-agent: *
Disallow: /

View File

@@ -30,7 +30,15 @@
<div class="row" id="site-body-wrapper">
{% include 'allianceauth/side-menu.html' %}
<div class="col-sm-10">
{% include 'allianceauth/messages.html' %}
{% if messages %}
<br>
{% for message in messages %}
<div class="alert alert-{{ message.level_tag }}">{{ message }}</div>
{% if not forloop.last %}
<br>
{% endif %}
{% endfor %}
{% endif %}
{% block content %}
{% endblock content %}
</div>
@@ -38,19 +46,20 @@
</div>
</div>
{% endif %}
<!-- share data with JS part -->
<div
id="dataExport"
data-notificationsListViewUrl="{% url 'notifications:list' %}"
data-notificationsRefreshTime="{% notifications_refresh_time %}"
data-userNotificationsCountViewUrl="{% url 'notifications:user_notifications_count' request.user.pk %}"
>
</div>
{% include 'bundles/bootstrap-js.html' %}
{% include 'bundles/jquery-visibility-js.html' %}
<script type="application/javascript">
let notificationUPdateSettings = {
notificationsListViewUrl: "{% url 'notifications:list' %}",
notificationsRefreshTime: "{% notifications_refresh_time %}",
userNotificationsCountViewUrl: "{% url 'notifications:user_notifications_count' request.user.pk %}"
};
</script>
<script src="{% static 'js/refresh_notifications.js' %}"></script>
{% block extra_javascript %}
{% block extra_javascript %}
{% endblock extra_javascript %}
<script>
{% block extra_script %}

View File

@@ -1,26 +0,0 @@
{% if messages %}
<br>
{% for message in messages %}
<div class="alert alert-{{ message.level_tag }} alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<div class="message-icon pull-left" style="margin-right: 0.5rem;">
{% if message.level_tag == "info" %}
<i class="fas fa-info-circle"></i>
{% elif message.level_tag == "success" %}
<i class="fas fa-check-circle"></i>
{% elif message.level_tag == "warning" %}
<i class="fas fa-exclamation-circle"></i>
{% elif message.level_tag == "danger" %}
<i class="fas fa-exclamation-triangle"></i>
{% endif %}
</div>
<div class="message-text" style="margin-left: 2.5rem;">
{{ message }}
</div>
</div>
{% endfor %}
{% endif %}

View File

@@ -70,8 +70,8 @@ def _current_notifications() -> dict:
_fetch_notification_issues_from_gitlab,
NOTIFICATION_CACHE_TIME
)
except requests.HTTPError:
logger.warning('Error while getting gitlab notifications', exc_info=True)
except requests.RequestException:
logger.exception('Error while getting gitlab notifications')
top_notifications = []
else:
if notifications:
@@ -95,8 +95,8 @@ def _current_version_summary() -> dict:
tags = cache.get_or_set(
'git_release_tags', _fetch_tags_from_gitlab, TAG_CACHE_TIME
)
except requests.HTTPError:
logger.warning('Error while getting gitlab release tags', exc_info=True)
except requests.RequestException:
logger.exception('Error while getting gitlab release tags')
return {}
if not tags:

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2 on 2021-04-07 16:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('timerboard', '0003_on_delete'),
]
operations = [
migrations.AlterField(
model_name='timer',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
]

View File

@@ -526,9 +526,7 @@
{% include 'bundles/moment-js.html' with locale=True %}
<script src="{% static 'js/timers.js' %}"></script>
<script type="application/javascript">
var locale = "{{ LANGUAGE_CODE }}";
var timers = [
let timers = [
{% for timer in timers %}
{
'id': {{ timer.id }},
@@ -545,67 +543,64 @@
{% endfor %}
];
moment.locale(locale);
/**
* Update a timer
* @param timer Timer information
*/
let updateTimer = function (timer) {
if (timer.targetDate.isAfter(Date.now())) {
let duration = moment.duration(timer.targetDate - moment(), 'milliseconds');
// Set initial values
setAllLocalTimes();
timedUpdate();
document.getElementById("countdown" + timer.id).innerHTML = getDurationString(duration);
} else {
timer.expired = true;
// Start timed updates
setInterval(timedUpdate, 1000);
document.getElementById("countdown" + timer.id).innerHTML = "";
}
};
let updateAllTimers = function () {
let l = timers.length;
for (var i=0; i < l; ++i) {
if (timers[i].expired) continue;
updateTimer(timers[i]);
}
};
function timedUpdate() {
updateClock();
updateAllTimers();
}
function updateAllTimers () {
var l = timers.length;
for (var i=0; i < l; ++i) {
if (timers[i].expired) continue;
updateTimer(timers[i]);
}
}
/**
* Update a timer
* Set the local time info for the timer
* @param timer Timer information
* @param timer.targetDate Date of the timer
* @param timer.id Id number of the timer
* @param timer.expired
*/
function updateTimer(timer) {
if (timer.targetDate.isAfter(Date.now())) {
duration = moment.duration(timer.targetDate - moment(), 'milliseconds');
document.getElementById("countdown" + timer.id).innerHTML = getDurationString(duration);
} else {
timer.expired = true;
document.getElementById("countdown" + timer.id).innerHTML = "";
}
}
let setLocalTime = function (timer) {
document.getElementById("localtime" + timer.id).innerHTML = timer.targetDate.format("ddd @ LT");
};
/**
* Set all local time fields
*/
function setAllLocalTimes() {
var l = timers.length;
let setAllLocalTimes = function () {
let l = timers.length;
for (var i=0; i < l; ++i) {
setLocalTime(timers[i]);
}
}
};
/**
* Set the local time info for the timer
* @param timer Timer information
* @param timer.targetDate Date of the timer
* @param timer.id Id number of the timer
*/
function setLocalTime(timer) {
document.getElementById("localtime" + timer.id).innerHTML = timer.targetDate.format("ddd @ LT");
}
function updateClock() {
let updateClock = function () {
document.getElementById("current-time").innerHTML = getCurrentEveTimeString();
}
};
// Set initial values
setAllLocalTimes();
timedUpdate();
// Start timed updates
setInterval(timedUpdate, 1000);
</script>
{% endblock content %}

View File

@@ -23,7 +23,7 @@ Within your auth project exists two folders named `static` and `templates`. Thes
You can add extra static or templates by putting files in these folders. Note that changes to static requires running the `python manage.py collectstatic` command to copy to the web server directory.
It is possible to overload static and templates shipped with Django or Alliance Auth by including a file with the exact path of the one you wish to overload. For instance if you wish to add extra links to the menu bar by editing the template, you would make a copy of the `allianceauth/templates/allianceauth/base.html` file to `myauth/templates/allianceauth/base.html` and edit it there. Notice the paths are identical after the `templates/` directory - this is critical for it to be recognized. Your custom template would be used instead of the one included with Alliance Auth when Django renders the web page. Similar idea for static: put CSS or images at an identical path after the `static/` directory and they will be copied to the web server directory instead of the ones included.
It is possible to overload static and templates shipped with Django or Alliance Auth by including a file with the exact path of the one you wish to overload. For instance if you wish to add extra links to the menu bar by editing the template, you would make a copy of the `allianceauth/templates/allianceauth/base.html` file to `myauth/templates/allinceauth/base.html` and edit it there. Notice the paths are identical after the `templates/` directory - this is critical for it to be recognized. Your custom template would be used instead of the one included with Alliance Auth when Django renders the web page. Similar idea for static: put CSS or images at an identical path after the `static/` directory and they will be copied to the web server directory instead of the ones included.
## Custom URLs and Views

View File

@@ -7,16 +7,19 @@ The notifications package has an API for sending notifications.
Location: ``allianceauth.notifications``
.. automodule:: allianceauth.notifications.__init__
:members: notify
:members: notify
:undoc-members:
models
===========
.. autoclass:: allianceauth.notifications.models.Notification
:members: Level, mark_viewed, set_level
:members: LEVEL_CHOICES, mark_viewed, set_level
:undoc-members:
managers
===========
.. autoclass:: allianceauth.notifications.managers.NotificationManager
:members: notify_user, user_unread_count
:undoc-members:

View File

@@ -26,7 +26,7 @@ DISCORD_SYNC_NAMES = False
CELERYBEAT_SCHEDULE['discord.update_all_usernames'] = {
'task': 'discord.update_all_usernames',
'schedule': crontab(minute=0, hour='*/12'),
'schedule': crontab(hour='*/12'),
}
```

View File

@@ -105,9 +105,7 @@ Click the URL provided to automatically connect to our server. It will prompt yo
Now we need to make groups. AllianceAuth handles groups in teamspeak differently: instead of creating groups it creates an association between groups in TeamSpeak and groups in AllianceAuth. Go ahead and make the groups you want to associate with auth groups, keeping in mind multiple TeamSpeak groups can be associated with a single auth group.
Navigate back to the AllianceAuth admin interface (example.com/admin) and under `Teamspeak3`, select `Auth / TS Groups`.
In the top-right corner click, first click on `Update TS3 Groups` to fetch the newly created server groups from TS3 (this may take a minute to complete). Then click on `Add Auth / TS Group` to link Auth groups with TS3 server groups.
Navigate back to the AllianceAuth admin interface (example.com/admin) and under `Services`, select `Auth / TS Groups`. In the top-right corner click `Add`.
The dropdown box provides all auth groups. Select one and assign TeamSpeak groups from the panels below. If these panels are empty, wait a minute for the database update to run, or see the [troubleshooting section](#ts-group-models-not-populating-on-admin-site) below.
@@ -121,23 +119,13 @@ To enable advanced permissions, on your client go to the `Tools` menu, `Applicat
### TS group models not populating on admin site
The method which populates these runs every 30 minutes. To populate manually you start the process from the admin site or from the Django shell.
#### Admin Site
Navigate to the AllianceAuth admin interface and under `Teamspeak3`, select `Auth / TS Groups`.
Then, in the top-right corner click, click on `Update TS3 Groups` to start the process of fetching the server groups from TS3 (this may take a minute to complete).
#### Django Shell
Start a django shell with:
The method which populates these runs every 30 minutes. To populate manually, start a django shell:
```bash
python manage.py shell
```
And execute the update as follows:
And execute the update:
```python
from allianceauth.services.modules.teamspeak3.tasks import Teamspeak3Tasks

View File

@@ -104,8 +104,6 @@ CREATE DATABASE alliance_auth CHARACTER SET utf8mb4;
GRANT ALL PRIVILEGES ON alliance_auth . * TO 'allianceserver'@'localhost';
```
Once your database is set up, you can leave the SQL shell with `exit`.
Add timezone tables to your mysql installation:
```bash
@@ -179,7 +177,7 @@ source /home/allianceserver/venv/auth/bin/activate
You need to have a dedicated Eve SSO app for Alliance auth. Please go to [EVE Developer](https://developers.eveonline.com/applications) to create one.
For **scopes** your SSO app needs to have at least `publicData`. Additional scopes depends on which Alliance Auth apps you will be using. For convenience, we recommend adding all available ESO scopes to your SSO app. Note that Alliance Auth will always ask the users to approve specific scopes before they are used.
For **scopes** your SSO app needs to have at least `publicData`. Additional scopes depends on which Alliance Auth apps you will be using. For convenience we recommend adding all available ESO scopes to your SSO app. Note that Alliance Auth will always ask the users to approve specific scopes before they are used.
As **callback URL** you want to define the URL of your Alliance Auth site plus the route: `/sso/callback`. Example for a valid callback URL: `https://auth.example.com/sso/callback`
@@ -238,7 +236,7 @@ Check to ensure your settings are valid.
python /home/allianceserver/myauth/manage.py check
```
Finally, ensure the allianceserver user has read/write permissions to this directory before proceeding.
And finally ensure the allianceserver user has read/write permissions to this directory before proceeding.
```bash
chown -R allianceserver:allianceserver /home/allianceserver/myauth
@@ -246,7 +244,7 @@ chown -R allianceserver:allianceserver /home/allianceserver/myauth
## Services
Alliance Auth needs some additional services to run, which we will set up and configure next.
Alliance Auth needs some additional services to run, which we will setup and configure next.
### Gunicorn
@@ -277,7 +275,7 @@ systemctl enable supervisord.service
systemctl start supervisord.service
```
Once installed, it needs a configuration file to know which processes to watch. Your Alliance Auth project comes with a ready-to-use template which will ensure the Celery workers, Celery task scheduler and Gunicorn are all running.
Once installed it needs a configuration file to know which processes to watch. Your Alliance Auth project comes with a ready-to-use template which will ensure the Celery workers, Celery task scheduler and Gunicorn are all running.
Ubuntu:
@@ -291,7 +289,7 @@ CentOS:
ln -s /home/allianceserver/myauth/supervisor.conf /etc/supervisord.d/myauth.ini
```
Activate it with `supervisorctl reload`.
And activate it with `supervisorctl reload`.
You can check the status of the processes with `supervisorctl status`. Logs from these processes are available in `/home/allianceserver/myauth/log` named by process.
@@ -306,11 +304,11 @@ You can check the status of the processes with `supervisorctl status`. Logs from
Once installed, decide on whether you're going to use [NGINX](nginx.md) or [Apache](apache.md) and follow the respective guide.
Note that Alliance Auth is designed to run with web servers on HTTPS. While running on HTTP is technically possible, it is not recommended for production use, and some functions (e.g. Email confirmation links) will not work properly.
Note that Alliance Auth is designed to run with web servers on HTTPS. While running on HTTP is technically possible, it is not recommended for production use and some functions (e.g. Email confirmation links) will not work properly.
## Superuser
Before using your auth site, it is essential to create a superuser account. This account will have all permissions in Alliance Auth. It's OK to use this as your personal auth account.
Before using your auth site it is essential to create a superuser account. This account will have all permissions in Alliance Auth. It's OK to use this as your personal auth account.
```bash
python /home/allianceserver/myauth/manage.py createsuperuser
@@ -318,7 +316,7 @@ python /home/allianceserver/myauth/manage.py createsuperuser
The superuser account is accessed by logging in via the admin site at `https://example.com/admin`.
If you intend to use this account as your personal auth account you need to add a main character. Navigate to the normal user dashboard (at `https://example.com`) after logging in via the admin site and select `Change Main`. Once a main character has been added, it is possible to use SSO to login to this account.
If you intend to use this account as your personal auth account you need to add a main character. Navigate to the normal user dashboard (at `https://example.com`) after logging in via the admin site and select `Change Main`. Once a main character has been added it is possible to use SSO to login to this account.
## Updating
@@ -342,7 +340,7 @@ Some releases come with new or changed models. Update your database to reflect t
python /home/allianceserver/myauth/manage.py migrate
```
Finally, some releases come with new or changed static files. Run the following command to update your static files folder:
Finally some releases come with new or changed static files. Run the following command to update your static files folder:
```bash
python /home/allianceserver/myauth/manage.py collectstatic

View File

@@ -45,17 +45,25 @@ Place your virtual host configuration in the appropriate section within `/etc/ht
```
<VirtualHost *:80>
ServerName auth.example.com
ProxyPassMatch ^/static !
ProxyPassMatch ^/robots.txt !
ProxyPass / http://127.0.0.1:8000/
ProxyPassReverse / http://127.0.0.1:8000/
ProxyPreserveHost On
Alias "/static" "/var/www/myauth/static"
Alias "/robots.txt" "/var/www/myauth/static/robots.txt"
<Directory "/var/www/myauth/static">
Require all granted
</Directory>
<Location "/robots.txt">
SetHandler None
Require all granted
</Location>
</VirtualHost>
```

View File

@@ -69,6 +69,10 @@ server {
autoindex off;
}
location /robots.txt {
alias /var/www/myauth/static/robots.txt;
}
# Gunicorn config goes below
location / {
include proxy_params;

View File

@@ -18,12 +18,17 @@ To run AA with a newer Python 3 version than your system's default you need to i
```eval_rst
.. note::
For stability and performance we currently recommend to run AA with Python 3.7. Since at the time of writing Python 3.7 was not available for CentOS through yum install this guide will upgrade to Python 3.6. For Ubuntu one can just replace "3.6" with "3.7" in the installation commands to get Python 3.7.
For stability and performance we currently recommend to run AA with Python 3.7. It has proven to be the fastest and most stable version in use currently.
```
To install other Python versions than come with your distro you need to add a new installation repository. Then you can install the specific Python 3 to your system.
To install other Python versions than those included with your distribution, you need to add a new installation repository. Then you can install the specific Python 3 to your system.
Ubuntu:
Ubuntu 1604 1804:
```eval_rst
.. note::
Ubuntu 2004 ships with Python 3.8, No updates required.
```
```bash
add-apt-repository ppa:deadsnakes/ppa
@@ -34,23 +39,38 @@ apt-get update
```
```bash
apt-get install python3.6 python3.6-dev python3.6-venv
apt-get install python3.7 python3.7-dev python3.7-venv
```
CentOS:
CentOS 7/8:
```bash
yum install https://centos7.iuscommunity.org/ius-release.rpm
cd ~
```
```bash
yum update
sudo yum install gcc openssl-devel bzip2-devel libffi-devel wget
```
```bash
yum install python36u python36u-pip python36u-devel
wget https://www.python.org/ftp/python/3.7.10/Python-3.7.10.tgz
```
```bash
tar xvf Python-3.7.10.tgz
```
```bash
cd Python-3.7.10/
```
```bash
./configure --enable-optimizations --enable-shared
```
```bash
make altinstall
```
## Preparing your venv
Before updating your venv it is important to make sure that your current installation is stable. Otherwise your new venv might not be consistent with your data, which might create problems.
@@ -97,12 +117,6 @@ If you unsure which apps you have installed from repos check `INSTALLED_APPS` in
pip list
```
Some AA installations might still be running an older version of django-celery-beat. We would recommend to upgrade to the current version before doing the Python update:
```bash
pip install -U 'django-celery-beat<2.00'
```
```bash
python manage.py migrate
```
@@ -171,7 +185,7 @@ mv /home/allianceserver/venv/auth /home/allianceserver/venv/auth_old
Now let's create our new venv with Python 3.6 and activate it:
```bash
python3.6 -m venv /home/allianceserver/venv/auth
python3.7 -m venv /home/allianceserver/venv/auth
```
```bash

View File

@@ -17,8 +17,8 @@ Mature Alliance Auth installations, or those with actively developed extensions
This can make it confusing for admins to apply the right permissions, contribute to larger queries in backend management or simply look unsightly.
```shell
python manage.py remove_stale_contenttypes --include-stale-apps
```python
python manage.py remove_stale_contenttypes --include-stale-apps
```
This inbuilt Django command will step through each contenttype and offer to delete it, displaying what exactly this will cascade to delete. Pay attention and ensure you understand exactly what is being removed before answering `yes`.

View File

@@ -22,12 +22,12 @@ install_requires = [
'celery>=4.3.0,<5.0.0,!=4.4.4', # 4.4.4 is missing a dependency
'celery_once>=2.0.1',
'django>=3.1.1,<3.2.0',
'django>=3.1.1,<4.0.0',
'django-bootstrap-form',
'django-registration>=3.1',
'django-sortedm2m',
'django-redis-cache>=3.0.0',
'django-celery-beat>=2.0.0,<2.2.1',
'django-celery-beat>=2.0.0',
'openfire-restapi',
'sleekxmpp',
@@ -58,7 +58,7 @@ setup(
extras_require={
'testing': testing_extras
},
python_requires='~=3.6',
python_requires='~=3.7',
license='GPLv2',
packages=['allianceauth'],
url=allianceauth.__url__,
@@ -72,15 +72,20 @@ setup(
'Environment :: Web Environment',
'Framework :: Django',
'Framework :: Django :: 3.1',
'Framework :: Django :: 3.2',
'Intended Audience :: Developers',
'License :: OSI Approved :: GNU General Public License v2 (GPLv2)',
'Operating System :: POSIX :: Linux',
'Programming Language :: Python',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
],
project_urls={
'Documentation': 'https://allianceauth.readthedocs.io/',
},
)

View File

@@ -1,16 +1,16 @@
[tox]
skipsdist = true
usedevelop = true
envlist = py{36,37,38}-{all}
envlist = py{37,38,39}-{all}
[testenv]
setenv =
all: DJANGO_SETTINGS_MODULE = tests.settings_all
core: DJANGO_SETTINGS_MODULE = tests.settings_core
basepython =
py36: python3.6
py37: python3.7
py38: python3.8
py39: python3.9
deps=
coverage
install_command = pip install -e ".[testing]" -U {opts} {packages}