Compare commits

..

40 Commits

Author SHA1 Message Date
Ariel Rin
321af5ec87 Version Bump v2.6.4 2020-04-17 06:56:40 +00:00
Ariel Rin
9ccf340b3d Merge branch 'error-redirects' into 'master'
Add 500 and 400, 403, 404 error redirects back to dashboard with basic message

See merge request allianceauth/allianceauth!1152
2020-04-17 06:45:01 +00:00
Aaron Kable
d7dcacb899 Add 500 and 400, 403, 404 error redirects back to dashboard with basic message 2020-04-17 06:45:01 +00:00
Ariel Rin
8addd483c2 Merge branch 'issue_1221' into 'master'
Remove support for Python 3.5

Closes #1224 and #1221

See merge request allianceauth/allianceauth!1182
2020-04-15 11:25:44 +00:00
Ariel Rin
4d27e5ac9b Merge branch 'improve_help_icon' into 'master'
Improve style of help icon

See merge request allianceauth/allianceauth!1192
2020-04-15 11:24:55 +00:00
ErikKalkoken
31290f6e80 Improve style of help icon 2020-04-11 20:23:41 +02:00
Ariel Rin
c31cc4dbee Merge branch 'mumble_displaynames' into 'master'
Mumble Display Names

See merge request allianceauth/allianceauth!1185
2020-04-06 02:19:53 +00:00
Aaron Kable
cc1f94cf61 Mumble Display Names 2020-04-06 02:19:53 +00:00
Ariel Rin
a9132b8d50 Merge branch 'fix_dev_docs' into 'master'
Add config file for readthedocs

See merge request allianceauth/allianceauth!1191
2020-04-03 13:33:30 +00:00
ErikKalkoken
7b4a9891aa Add config file for readthedocs 2020-04-03 15:18:45 +02:00
Ariel Rin
dcaaf38ecc Merge branch 'esi_update' into 'master'
Update swagger files and remove swagger file dependency from srp package

See merge request allianceauth/allianceauth!1187
2020-04-03 12:39:34 +00:00
Ariel Rin
653a8aa850 Merge branch 'improve_docu_link' into 'master'
Move docu link to top menu and open in new window

See merge request allianceauth/allianceauth!1189
2020-04-03 12:04:06 +00:00
Ariel Rin
274af11385 Merge branch 'docu_devs_update' into 'master'
Extend developer docs

See merge request allianceauth/allianceauth!1188
2020-04-03 12:03:34 +00:00
Erik Kalkoken
170b246901 Extend developer docs 2020-04-03 12:03:34 +00:00
Ariel Rin
5250432ce3 Version Bump v2.6.3 2020-04-02 03:51:59 +00:00
Ariel Rin
53d6e973eb Merge branch 'i18n-chinese' into 'master'
Update Translations from Transifex

See merge request allianceauth/allianceauth!1190
2020-04-02 03:32:39 +00:00
Ariel Rin
c9bdd62d53 Update Translations from Transifex 2020-04-02 03:32:39 +00:00
Ariel Rin
7eb98af528 Merge branch 'issue_1225' into 'master'
Fix broken link and remove outdated migrations for services name formatter

Closes #1225

See merge request allianceauth/allianceauth!1183
2020-04-02 03:04:55 +00:00
Ariel Rin
385e3e21b3 Merge branch 'improve_groups_view' into 'master'
Add sorting to group view and add tests to group management

See merge request allianceauth/allianceauth!1172
2020-04-02 03:01:27 +00:00
Erik Kalkoken
127ec63d76 Add sorting to group view and add tests to group management 2020-04-02 03:01:27 +00:00
Ariel Rin
4988b5f260 Merge branch 'common_logger' into 'master'
Extensions Logging

See merge request allianceauth/allianceauth!1180
2020-04-02 02:59:55 +00:00
Ariel Rin
f28a50f92c Merge branch 'fix_translations_3' into 'master'
Add missing translations

See merge request allianceauth/allianceauth!1186
2020-03-26 03:06:11 +00:00
Ariel Rin
e8efe8e609 Merge branch 'i18n-chinese' into 'master'
Add Korean and Russian, Update from Transifex

See merge request allianceauth/allianceauth!1179
2020-03-26 02:50:25 +00:00
Ariel Rin
d7e7457bc5 Add Korean and Russian, Update from Transifex 2020-03-26 02:50:25 +00:00
Ariel Rin
daff927811 Merge branch 'prioritize_celery' into 'master'
Add Celery Priorities

See merge request allianceauth/allianceauth!1181
2020-03-26 02:20:02 +00:00
Aaron Kable
8861ec0a61 Add Celery Priorities 2020-03-26 02:20:02 +00:00
Ariel Rin
bd4321f61a Merge branch 'docs_update' into 'master'
Docs only: Harmonize gunicorn config, add localization feature

See merge request allianceauth/allianceauth!1184
2020-03-26 01:55:03 +00:00
Erik Kalkoken
d831482fe0 Docs only: Harmonize gunicorn config, add localization feature 2020-03-26 01:55:03 +00:00
ErikKalkoken
9ea79ea389 Move docu link to top menu and open in new window 2020-03-26 00:10:30 +01:00
ErikKalkoken
b6fdf840ef Update swagger files and remove swagger fle dependency from srp package 2020-03-25 18:00:23 +01:00
ErikKalkoken
73f262ce4b Add missing translations 2020-03-24 20:21:35 +01:00
ErikKalkoken
f63434adc3 Fix broken link and remove outdated migrations for services name formatter 2020-03-21 14:41:45 +01:00
ErikKalkoken
42948386ec Remove support for Python 3.5 2020-03-21 13:16:42 +01:00
Ariel Rin
32e0621b0a Merge branch 'improve_install_docu' into 'master'
Docs: Add python upgrade guide, remove old AA 1.15 upgrade guide, improve install guide

See merge request allianceauth/allianceauth!1177
2020-03-15 12:21:21 +00:00
Erik Kalkoken
78e05b84e9 Docs: Add python upgrade guide, remove old AA 1.15 upgrade guide, improve install guide 2020-03-15 12:21:21 +00:00
Col Crunch
76ebd21163 Add function to services.hooks to provide a concise way for creating loggers for extensions/plugins. Revise basic documentation to use this function. 2020-03-13 15:21:15 -04:00
Col Crunch
38aaf545c6 Add some very basic docs for logging changes 2020-03-13 14:42:09 -04:00
Col Crunch
527d7ef671 Change level of extension_file handler, rename the logger from allianceauth.extensions to extensions and remove propagate from the logger. 2020-03-13 04:42:09 -04:00
Col Crunch
e54b80e061 Add a common logger (and specific log file) for extensions to utilize. 2020-03-13 00:33:35 -04:00
Col Crunch
27f95a8b2c Remove Zone.Indentifier files. 2020-03-12 23:55:34 -04:00
110 changed files with 8495 additions and 731 deletions

3
.gitignore vendored
View File

@@ -72,3 +72,6 @@ celerybeat-schedule
#transifex
.tx/
#other
.flake8

View File

@@ -6,11 +6,6 @@ before_script:
- python -V
- pip install wheel tox
test-3.5-core:
image: python:3.5-buster
script:
- tox -e py35-core
test-3.6-core:
image: python:3.6-buster
script:
@@ -26,11 +21,6 @@ test-3.8-core:
script:
- tox -e py38-core
test-3.5-all:
image: python:3.5-buster
script:
- tox -e py35-all
test-3.6-all:
image: python:3.6-buster
script:

27
.readthedocs.yml Normal file
View File

@@ -0,0 +1,27 @@
# .readthedocs.yml
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
# Required
version: 2
# Build documentation in the docs/ directory with Sphinx
sphinx:
configuration: docs/conf.py
# Build documentation with MkDocs
#mkdocs:
# configuration: mkdocs.yml
# Optionally build your docs in additional formats such as PDF and ePub
formats: all
# Optionally set the version of Python and requirements required to build your docs
python:
version: 3.7
install:
- method: pip
path: .
extra_requirements:
- testing
system_packages: true

View File

@@ -11,32 +11,34 @@
An auth system for EVE Online to help in-game organizations manage online service access.
## Contens
## Content
- [Overview](#overview)
- [Documentation](http://allianceauth.rtfd.io)
- [Support](#support)
- [Release Notes](https://gitlab.com/allianceauth/allianceauth/-/releases)
- [Devloper Team](#developer-team)
- [Developer Team](#developer-team)
- [Contributing](#contributing)
## Overview
Alliance Auth (AA) is a web application that helps Eve Online organizations efficiently manage access to their applications and services.
Alliance Auth (AA) is a web site that helps Eve Online organizations efficiently manage access to applications and services.
Main features:
- Automatically grants or revokes user access to external applications / services (e.g. Discord, Mumble) and web apps (e.g. SRP requests) based on the user's current membership to [in-game organizations](https://allianceauth.readthedocs.io/en/latest/features/states/) and [groups](https://allianceauth.readthedocs.io/en/latest/features/groups/)
- Automatically grants or revokes user access to external services (e.g. Discord, Mumble) and web apps (e.g. SRP requests) based on the user's current membership to [in-game organizations](https://allianceauth.readthedocs.io/en/latest/features/core/states/) and [groups](https://allianceauth.readthedocs.io/en/latest/features/core/groups/)
- Provides a central web site where users can directly access web apps (e.g. SRP requests, Fleet Schedule) and manage their access to external services and groups.
- Includes a set of connectors (called ["services"](https://allianceauth.readthedocs.io/en/latest/installation/services/)) for integrating access management with many popular external applications / services like Discord, Mumble, Teamspeak 3, SMF and others
- Includes a set of connectors (called ["services"](https://allianceauth.readthedocs.io/en/latest/features/services/)) for integrating access management with many popular external applications / services like Discord, Mumble, Teamspeak 3, SMF and others
- Includes a set of web apps called ["plug-in apps"](https://allianceauth.readthedocs.io/en/latest/features/) which add many useful functions: fleet schedule, timer board, SRP request management, fleet activity tracker and character application management
- Includes a set of web [apps](https://allianceauth.readthedocs.io/en/latest/features/apps/) which add many useful functions, e.g.: fleet schedule, timer board, SRP request management, fleet activity tracker
- Can be easily extended with new services and plugin-apps. Many additional services and plugin-apps are provided by the community and can be found here: [Community Creations](https://gitlab.com/allianceauth/community-creations)
- Can be easily extended with additional services and apps. Many are provided by the community and can be found here: [Community Creations](https://gitlab.com/allianceauth/community-creations)
For further details about AA - including an installation guide and a full list of included services and plugin apps - please see the [offical documentation](http://allianceauth.rtfd.io).
- Chinese :cn:, English :us:, German :de: and Spanish :es: localization
For further details about AA - including an installation guide and a full list of included services and plugin apps - please see the [official documentation](http://allianceauth.rtfd.io).
## Screenshot

View File

@@ -1,6 +1,6 @@
# This will make sure the app is always imported when
# Django starts so that shared_task will use this app.
__version__ = '2.6.2'
__version__ = '2.6.4'
NAME = 'Alliance Auth v%s' % __version__
default_app_config = 'allianceauth.apps.AllianceAuthConfig'

View File

@@ -506,7 +506,7 @@ class BaseOwnershipAdmin(admin.ModelAdmin):
'character',
)
search_fields = (
'user__user',
'user__username',
'character__character_name',
'character__corporation_name',
'character__alliance_name'

View File

@@ -2,10 +2,17 @@ from django.urls import reverse
def get_admin_change_view_url(obj: object) -> str:
"""returns URL to admin change view for given object"""
return reverse(
'admin:{}_{}_change'.format(
obj._meta.app_label,
type(obj).__name__.lower()
obj._meta.app_label, type(obj).__name__.lower()
),
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,3 +1,4 @@
from urllib.parse import quote
from unittest.mock import patch
from django.conf import settings
@@ -6,8 +7,9 @@ from django.contrib.admin.sites import AdminSite
from django.contrib.auth.models import User as BaseUser, Group
from django.test import TestCase, RequestFactory, Client
from allianceauth.authentication.models import CharacterOwnership, State, \
OwnershipRecord
from allianceauth.authentication.models import (
CharacterOwnership, State, OwnershipRecord
)
from allianceauth.eveonline.models import (
EveCharacter, EveCorporationInfo, EveAllianceInfo
)
@@ -28,7 +30,7 @@ from ..admin import (
user_username,
update_main_character_model
)
from . import get_admin_change_view_url
from . import get_admin_change_view_url, get_admin_search_url
if 'allianceauth.eveonline.autogroups' in settings.INSTALLED_APPS:
_has_auto_groups = True
@@ -175,6 +177,17 @@ def create_test_data():
return user_1, user_2, user_3, group_1, group_2
def make_generic_search_request(ModelClass: type, search_term: str):
User.objects.create_superuser(
username='superuser', password='secret', email='admin@example.com'
)
c = Client()
c.login(username='superuser', password='secret')
return c.get(
'%s?q=%s' % (get_admin_search_url(ModelClass), quote(search_term))
)
class TestCharacterOwnershipAdmin(TestCase):
@classmethod
@@ -197,6 +210,14 @@ class TestCharacterOwnershipAdmin(TestCase):
response = c.get(get_admin_change_view_url(ownership))
self.assertEqual(response.status_code, 200)
def test_search_works(self):
obj = CharacterOwnership.objects\
.filter(user=self.user_1)\
.first()
response = make_generic_search_request(type(obj), obj.user.username)
expected = 200
self.assertEqual(response.status_code, expected)
class TestOwnershipRecordAdmin(TestCase):
@@ -222,6 +243,12 @@ class TestOwnershipRecordAdmin(TestCase):
response = c.get(get_admin_change_view_url(ownership_record))
self.assertEqual(response.status_code, 200)
def test_search_works(self):
obj = OwnershipRecord.objects.first()
response = make_generic_search_request(type(obj), obj.user.username)
expected = 200
self.assertEqual(response.status_code, expected)
class TestStateAdmin(TestCase):
@@ -250,6 +277,11 @@ class TestStateAdmin(TestCase):
response = c.get(get_admin_change_view_url(member_state))
self.assertEqual(response.status_code, 200)
def test_search_works(self):
obj = State.objects.first()
response = make_generic_search_request(type(obj), obj.name)
expected = 200
self.assertEqual(response.status_code, expected)
class TestUserAdmin(TestCase):
@@ -541,3 +573,9 @@ class TestUserAdmin(TestCase):
c.login(username='superuser', password='secret')
response = c.get(get_admin_change_view_url(self.user_1))
self.assertEqual(response.status_code, 200)
def test_search_works(self):
obj = User.objects.first()
response = make_generic_search_request(type(obj), obj.username)
expected = 200
self.assertEqual(response.status_code, expected)

View File

@@ -22,13 +22,6 @@ urlpatterns = [
r'^account/characters/add/$',
views.add_character,
name='add_character'
),
url(
r'^help/$',
login_required(
TemplateView.as_view(template_name='allianceauth/help.html')
),
name='help'
),
),
url(r'^dashboard/$', views.dashboard, name='dashboard'),
]

View File

@@ -8,7 +8,7 @@ from django.contrib.auth.models import User
from django.core import signing
from django.urls import reverse
from django.shortcuts import redirect, render
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from allianceauth.eveonline.models import EveCharacter
from esi.decorators import token_required
@@ -69,7 +69,10 @@ def main_character_change(request, token):
if not CharacterOwnership.objects.filter(character__character_id=token.character_id).exists():
co = CharacterOwnership.objects.create_by_token(token)
else:
messages.error(request, 'Cannot change main character to %(char)s: character owned by a different account.' % ({'char': token.character_name}))
messages.error(
request,
_('Cannot change main character to %(char)s: character owned by a different account.') % ({'char': token.character_name})
)
co = None
if co:
request.user.profile.main_character = co.character

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -7,6 +7,7 @@ from .models import EveCorporationInfo
logger = logging.getLogger(__name__)
TASK_PRIORITY = 7
@shared_task
def update_corp(corp_id):
@@ -27,11 +28,12 @@ def update_character(character_id):
def run_model_update():
# update existing corp models
for corp in EveCorporationInfo.objects.all().values('corporation_id'):
update_corp.delay(corp['corporation_id'])
update_corp.apply_async(args=[corp['corporation_id']], priority=TASK_PRIORITY)
# update existing alliance models
for alliance in EveAllianceInfo.objects.all().values('alliance_id'):
update_alliance.delay(alliance['alliance_id'])
update_alliance.apply_async(args=[alliance['alliance_id']], priority=TASK_PRIORITY)
#update existing character models
for character in EveCharacter.objects.all().values('character_id'):
update_character.delay(character['character_id'])
update_character.apply_async(args=[character['character_id']], priority=TASK_PRIORITY)

View File

@@ -80,28 +80,28 @@ class TestTasks(TestCase):
character_name='character.name',
corporation_id='character.corp.id',
corporation_name='character.corp.name',
corporation_ticker='character.corp.ticker',
corporation_ticker='c.c.t', # max 5 chars
alliance_id='character.alliance.id',
alliance_name='character.alliance.name',
)
run_model_update()
self.assertEqual(mock_update_corp.delay.call_count, 1)
self.assertEqual(mock_update_corp.apply_async.call_count, 1)
self.assertEqual(
int(mock_update_corp.delay.call_args[0][0]),
int(mock_update_corp.apply_async.call_args[1]['args'][0]),
2345
)
self.assertEqual(mock_update_alliance.delay.call_count, 1)
self.assertEqual(mock_update_alliance.apply_async.call_count, 1)
self.assertEqual(
int(mock_update_alliance.delay.call_args[0][0]),
int(mock_update_alliance.apply_async.call_args[1]['args'][0]),
3456
)
self.assertEqual(mock_update_character.delay.call_count, 1)
self.assertEqual(mock_update_character.apply_async.call_count, 1)
self.assertEqual(
int(mock_update_character.delay.call_args[0][0]),
int(mock_update_character.apply_async.call_args[1]['args'][0]),
1234
)

File diff suppressed because one or more lines are too long

View File

@@ -1,27 +1,53 @@
from django.contrib.auth.models import Group
from django.db.models import Q
import logging
from django.contrib.auth.models import Group, User
from django.db.models import Q, QuerySet
from allianceauth.authentication.models import State
logger = logging.getLogger(__name__)
class GroupManager:
def __init__(self):
pass
@classmethod
def get_joinable_groups_for_user(
cls, user: User, include_hidden = True
) -> QuerySet:
"""get groups a user could join incl. groups already joined"""
groups_qs = cls.get_joinable_groups(user.profile.state)
if not user.has_perm('groupmanagement.request_groups'):
groups_qs = groups_qs.filter(authgroup__public=True)
if not include_hidden:
groups_qs = groups_qs.filter(authgroup__hidden=False)
return groups_qs
@staticmethod
def get_joinable_groups(state):
return Group.objects.select_related('authgroup').exclude(authgroup__internal=True)\
def get_joinable_groups(state: State) -> QuerySet:
"""get groups that can be joined by user with given state"""
return Group.objects\
.select_related('authgroup')\
.exclude(authgroup__internal=True)\
.filter(Q(authgroup__states=state) | Q(authgroup__states=None))
@staticmethod
def get_all_non_internal_groups():
return Group.objects.select_related('authgroup').exclude(authgroup__internal=True)
def get_all_non_internal_groups() -> QuerySet:
"""get groups that are not internal"""
return Group.objects\
.select_related('authgroup')\
.exclude(authgroup__internal=True)
@staticmethod
def get_group_leaders_groups(user):
def get_group_leaders_groups(user: User):
return Group.objects.select_related('authgroup').filter(authgroup__group_leaders__in=[user]) | \
Group.objects.select_related('authgroup').filter(authgroup__group_leader_groups__in=user.groups.all())
@staticmethod
def joinable_group(group, state):
def joinable_group(group: Group, state: State) -> bool:
"""
Check if a group is a user/state joinable group, i.e.
not an internal group for Corp, Alliance, Members etc,
@@ -30,12 +56,15 @@ class GroupManager:
:param state: allianceauth.authentication.State object
:return: bool True if its joinable, False otherwise
"""
if len(group.authgroup.states.all()) != 0 and state not in group.authgroup.states.all():
if (len(group.authgroup.states.all()) != 0
and state not in group.authgroup.states.all()
):
return False
return not group.authgroup.internal
else:
return not group.authgroup.internal
@staticmethod
def check_internal_group(group):
def check_internal_group(group: Group) -> bool:
"""
Check if a group is auditable, i.e not an internal group
:param group: django.contrib.auth.models.Group object
@@ -44,20 +73,11 @@ class GroupManager:
return not group.authgroup.internal
@staticmethod
def check_internal_group(group):
"""
Check if a group is auditable, i.e not an internal group
:param group: django.contrib.auth.models.Group object
:return: bool True if it is auditable, false otherwise
"""
return not group.authgroup.internal
@staticmethod
def has_management_permission(user):
def has_management_permission(user: User) -> bool:
return user.has_perm('auth.group_management')
@classmethod
def can_manage_groups(cls, user):
def can_manage_groups(cls, user:User ) -> bool:
"""
For use with user_passes_test decorator.
Check if the user can manage groups. Either has the
@@ -71,7 +91,7 @@ class GroupManager:
return False
@classmethod
def can_manage_group(cls, user, group):
def can_manage_group(cls, user: User, group: Group) -> bool:
"""
Check user has permission to manage the given group
:param user: User object to test permission of

View File

@@ -15,7 +15,7 @@
<div class="panel-body">
<p>
<a class="btn btn-default" href="{% url 'groupmanagement:membership' %}" role="button">
Back
{% trans "Back" %}
</a>
</p>
{% if entries %}

View File

@@ -17,7 +17,7 @@
<div class="panel-body">
<p>
<a class="btn btn-default" href="{% url 'groupmanagement:membership' %}" role="button">
Back
{% trans "Back" %}
</a>
</p>
{% if group.user_set %}

View File

@@ -11,7 +11,7 @@
{% include 'groupmanagement/menu.html' %}
<div class="panel panel-default">
<div class="panel-heading">
Groups
{% trans "Groups" %}
</div>
<div class="panel-body">
{% if groups %}

View File

@@ -1,107 +0,0 @@
from unittest import mock
from django.test import TestCase
from allianceauth.tests.auth_utils import AuthUtils
from allianceauth.eveonline.models import EveCorporationInfo, EveAllianceInfo, EveCharacter
from django.contrib.auth.models import User, Group
from allianceauth.groupmanagement.managers import GroupManager
from allianceauth.groupmanagement.signals import check_groups_on_state_change
class GroupManagementVisibilityTestCase(TestCase):
@classmethod
def setUpTestData(cls):
cls.user = AuthUtils.create_user('test')
AuthUtils.add_main_character(cls.user, 'test character', '1', corp_id='2', corp_name='test_corp', corp_ticker='TEST', alliance_id='3', alliance_name='TEST')
cls.user.profile.refresh_from_db()
cls.alliance = EveAllianceInfo.objects.create(alliance_id='3', alliance_name='test alliance', alliance_ticker='TEST', executor_corp_id='2')
cls.corp = EveCorporationInfo.objects.create(corporation_id='2', corporation_name='test corp', corporation_ticker='TEST', alliance=cls.alliance, member_count=1)
cls.group1 = Group.objects.create(name='group1')
cls.group2 = Group.objects.create(name='group2')
cls.group3 = Group.objects.create(name='group3')
def setUp(self):
self.user.refresh_from_db()
def _refresh_user(self):
self.user = User.objects.get(pk=self.user.pk)
def test_get_group_leaders_groups(self):
self.group1.authgroup.group_leaders.add(self.user)
self.group2.authgroup.group_leader_groups.add(self.group1)
self._refresh_user()
groups = GroupManager.get_group_leaders_groups(self.user)
self.assertIn(self.group1, groups) #avail due to user
self.assertNotIn(self.group2, groups) #not avail due to group
self.assertNotIn(self.group3, groups) #not avail at all
self.user.groups.add(self.group1)
self._refresh_user()
groups = GroupManager.get_group_leaders_groups(self.user)
self.assertIn(self.group1, groups) #avail due to user
self.assertIn(self.group2, groups) #avail due to group1
self.assertNotIn(self.group3, groups) #not avail at all
def test_can_manage_group(self):
self.group1.authgroup.group_leaders.add(self.user)
self.user.groups.add(self.group1)
self._refresh_user()
self.assertTrue(GroupManager.can_manage_group(self.user, self.group1))
self.assertFalse(GroupManager.can_manage_group(self.user, self.group2))
self.assertFalse(GroupManager.can_manage_group(self.user, self.group3))
self.group2.authgroup.group_leader_groups.add(self.group1)
self.group1.authgroup.group_leaders.remove(self.user)
self._refresh_user()
self.assertFalse(GroupManager.can_manage_group(self.user, self.group1))
self.assertTrue(GroupManager.can_manage_group(self.user, self.group2))
self.assertFalse(GroupManager.can_manage_group(self.user, self.group3))
class GroupManagementStateTestCase(TestCase):
@classmethod
def setUpTestData(cls):
cls.user = AuthUtils.create_user('test')
AuthUtils.add_main_character(cls.user, 'test character', '1', corp_id='2', corp_name='test_corp', corp_ticker='TEST', alliance_id='3', alliance_name='TEST')
cls.user.profile.refresh_from_db()
cls.alliance = EveAllianceInfo.objects.create(alliance_id='3', alliance_name='test alliance', alliance_ticker='TEST', executor_corp_id='2')
cls.corp = EveCorporationInfo.objects.create(corporation_id='2', corporation_name='test corp', corporation_ticker='TEST', alliance=cls.alliance, member_count=1)
cls.state_group = Group.objects.create(name='state_group')
cls.open_group = Group.objects.create(name='open_group')
cls.state = AuthUtils.create_state('test state', 500)
cls.state_group.authgroup.states.add(cls.state)
cls.state_group.authgroup.internal = False
cls.state_group.save()
def setUp(self):
self.user.refresh_from_db()
self.state.refresh_from_db()
def _refresh_user(self):
self.user = User.objects.get(pk=self.user.pk)
def _refresh_test_group(self):
self.state_group = Group.objects.get(pk=self.state_group.pk)
def test_drop_state_group(self):
self.user.groups.add(self.open_group)
self.user.groups.add(self.state_group)
self.assertEqual(self.user.profile.state.name, "Guest")
self.state.member_corporations.add(self.corp)
self._refresh_user()
self.assertEqual(self.user.profile.state, self.state)
groups = self.user.groups.all()
self.assertIn(self.state_group, groups) #keeps group
self.assertIn(self.open_group, groups) #public group unafected
self.state.member_corporations.clear()
self._refresh_user()
self.assertEqual(self.user.profile.state.name, "Guest")
groups = self.user.groups.all()
self.assertNotIn(self.state_group, groups) #looses group
self.assertIn(self.open_group, groups) #public group unafected

View File

@@ -0,0 +1,337 @@
from unittest.mock import Mock, patch
from django.contrib.auth.models import Group, User
from django.test import TestCase
from django.urls import reverse
from allianceauth.eveonline.models import EveCorporationInfo, EveAllianceInfo
from allianceauth.tests.auth_utils import AuthUtils
from ..models import AuthGroup
from ..managers import GroupManager
class MockUserNotAuthenticated():
def __init__(self):
self.is_authenticated = False
class GroupManagementVisibilityTestCase(TestCase):
@classmethod
def setUpTestData(cls):
cls.user = AuthUtils.create_user('test')
AuthUtils.add_main_character(
cls.user, 'test character', '1', corp_id='2', corp_name='test_corp', corp_ticker='TEST', alliance_id='3', alliance_name='TEST'
)
cls.user.profile.refresh_from_db()
cls.alliance = EveAllianceInfo.objects.create(alliance_id='3', alliance_name='test alliance', alliance_ticker='TEST', executor_corp_id='2')
cls.corp = EveCorporationInfo.objects.create(
corporation_id='2', corporation_name='test corp', corporation_ticker='TEST', alliance=cls.alliance, member_count=1
)
cls.group1 = Group.objects.create(name='group1')
cls.group2 = Group.objects.create(name='group2')
cls.group3 = Group.objects.create(name='group3')
def setUp(self):
self.user.refresh_from_db()
def _refresh_user(self):
self.user = User.objects.get(pk=self.user.pk)
def test_get_group_leaders_groups(self):
self.group1.authgroup.group_leaders.add(self.user)
self.group2.authgroup.group_leader_groups.add(self.group1)
self._refresh_user()
groups = GroupManager.get_group_leaders_groups(self.user)
self.assertIn(self.group1, groups) #avail due to user
self.assertNotIn(self.group2, groups) #not avail due to group
self.assertNotIn(self.group3, groups) #not avail at all
self.user.groups.add(self.group1)
self._refresh_user()
groups = GroupManager.get_group_leaders_groups(self.user)
def test_can_manage_group(self):
self.group1.authgroup.group_leaders.add(self.user)
self.user.groups.add(self.group1)
self._refresh_user()
self.assertTrue(GroupManager.can_manage_group(self.user, self.group1))
self.assertFalse(GroupManager.can_manage_group(self.user, self.group2))
self.assertFalse(GroupManager.can_manage_group(self.user, self.group3))
self.group2.authgroup.group_leader_groups.add(self.group1)
self.group1.authgroup.group_leaders.remove(self.user)
self._refresh_user()
self.assertFalse(GroupManager.can_manage_group(self.user, self.group1))
self.assertTrue(GroupManager.can_manage_group(self.user, self.group2))
self.assertFalse(GroupManager.can_manage_group(self.user, self.group3))
class TestGroupManager(TestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
# group 1
cls.group_default = Group.objects.create(name='default')
cls.group_default.authgroup.description = 'Default Group'
cls.group_default.authgroup.internal = False
cls.group_default.authgroup.hidden = False
cls.group_default.authgroup.save()
# group 2
cls.group_internal = Group.objects.create(name='internal')
cls.group_internal.authgroup.description = 'Internal Group'
cls.group_internal.authgroup.internal = True
cls.group_internal.authgroup.save()
# group 3
cls.group_hidden = Group.objects.create(name='hidden')
cls.group_hidden.authgroup.description = 'Hidden Group'
cls.group_hidden.authgroup.internal = False
cls.group_hidden.authgroup.hidden = True
cls.group_hidden.authgroup.save()
# group 4
cls.group_open = Group.objects.create(name='open')
cls.group_open.authgroup.description = 'Open Group'
cls.group_open.authgroup.internal = False
cls.group_open.authgroup.hidden = False
cls.group_open.authgroup.open = True
cls.group_open.authgroup.save()
# group 5
cls.group_public_1 = Group.objects.create(name='public 1')
cls.group_public_1.authgroup.description = 'Public Group 1'
cls.group_public_1.authgroup.internal = False
cls.group_public_1.authgroup.hidden = False
cls.group_public_1.authgroup.public = True
cls.group_public_1.authgroup.save()
# group 6
cls.group_public_2 = Group.objects.create(name='public 2')
cls.group_public_2.authgroup.description = 'Public Group 2'
cls.group_public_2.authgroup.internal = False
cls.group_public_2.authgroup.hidden = True
cls.group_public_2.authgroup.open = True
cls.group_public_2.authgroup.public = True
cls.group_public_2.authgroup.save()
# group 7
cls.group_default_member = Group.objects.create(name='default members')
cls.group_default_member.authgroup.description = \
'Default Group for members only'
cls.group_default_member.authgroup.internal = False
cls.group_default_member.authgroup.hidden = False
cls.group_default_member.authgroup.open = False
cls.group_default_member.authgroup.public = False
cls.group_default_member.authgroup.states.add(
AuthUtils.get_member_state()
)
cls.group_default_member.authgroup.save()
def setUp(self):
self.user = AuthUtils.create_user('Bruce Wayne')
def test_get_joinable_group_member(self):
result = GroupManager.get_joinable_groups(
AuthUtils.get_member_state()
)
expected = {
self.group_default,
self.group_hidden,
self.group_open,
self.group_public_1,
self.group_public_2,
self.group_default_member
}
self.assertSetEqual(set(result), expected)
def test_get_joinable_group_guest(self):
result = GroupManager.get_joinable_groups(
AuthUtils.get_guest_state()
)
expected = {
self.group_default,
self.group_hidden,
self.group_open,
self.group_public_1,
self.group_public_2
}
self.assertSetEqual(set(result), expected)
def test_joinable_group_member(self):
member_state = AuthUtils.get_member_state()
for x in [
self.group_default,
self.group_hidden,
self.group_open,
self.group_public_1,
self.group_public_2,
self.group_default_member
]:
self.assertTrue(GroupManager.joinable_group(x, member_state))
for x in [
self.group_internal,
]:
self.assertFalse(GroupManager.joinable_group(x, member_state))
def test_joinable_group_guest(self):
guest_state = AuthUtils.get_guest_state()
for x in [
self.group_default,
self.group_hidden,
self.group_open,
self.group_public_1,
self.group_public_2
]:
self.assertTrue(GroupManager.joinable_group(x, guest_state))
for x in [
self.group_internal,
self.group_default_member
]:
self.assertFalse(GroupManager.joinable_group(x, guest_state))
def test_get_all_non_internal_groups(self):
result = GroupManager.get_all_non_internal_groups()
expected = {
self.group_default,
self.group_hidden,
self.group_open,
self.group_public_1,
self.group_public_2,
self.group_default_member
}
self.assertSetEqual(set(result), expected)
def test_check_internal_group(self):
self.assertTrue(
GroupManager.check_internal_group(self.group_default)
)
self.assertFalse(
GroupManager.check_internal_group(self.group_internal)
)
def test_get_joinable_groups_for_user_no_permission(self):
AuthUtils.assign_state(self.user, AuthUtils.get_guest_state())
result = GroupManager.get_joinable_groups_for_user(self.user)
expected= {self.group_public_1, self.group_public_2}
self.assertSetEqual(set(result), expected)
def test_get_joinable_groups_for_user_guest_w_permission_(self):
AuthUtils.assign_state(self.user, AuthUtils.get_guest_state())
AuthUtils.add_permission_to_user_by_name(
'groupmanagement.request_groups', self.user
)
result = GroupManager.get_joinable_groups_for_user(self.user)
expected = {
self.group_default,
self.group_hidden,
self.group_open,
self.group_public_1,
self.group_public_2
}
self.assertSetEqual(set(result), expected)
def test_get_joinable_groups_for_user_member_w_permission(self):
AuthUtils.assign_state(self.user, AuthUtils.get_member_state(), True)
AuthUtils.add_permission_to_user_by_name(
'groupmanagement.request_groups', self.user
)
result = GroupManager.get_joinable_groups_for_user(self.user)
expected = {
self.group_default,
self.group_hidden,
self.group_open,
self.group_public_1,
self.group_public_2,
self.group_default_member
}
self.assertSetEqual(set(result), expected)
def test_get_joinable_groups_for_user_member_w_permission_no_hidden(self):
AuthUtils.assign_state(self.user, AuthUtils.get_member_state(), True)
AuthUtils.add_permission_to_user_by_name(
'groupmanagement.request_groups', self.user
)
result = GroupManager.get_joinable_groups_for_user(
self.user, include_hidden=False
)
expected = {
self.group_default,
self.group_open,
self.group_public_1,
self.group_default_member
}
self.assertSetEqual(set(result), expected)
def test_has_management_permission(self):
user = AuthUtils.create_user('Clark Kent')
AuthUtils.add_permission_to_user_by_name(
'auth.group_management', user
)
self.assertTrue(GroupManager.has_management_permission(user))
def test_can_manage_groups_no_perm_no_group(self):
user = AuthUtils.create_user('Clark Kent')
self.assertFalse(GroupManager.can_manage_groups(user))
def test_can_manage_groups_user_not_authenticated(self):
user = MockUserNotAuthenticated()
self.assertFalse(GroupManager.can_manage_groups(user))
def test_can_manage_groups_has_perm(self):
user = AuthUtils.create_user('Clark Kent')
AuthUtils.add_permission_to_user_by_name(
'auth.group_management', user
)
self.assertTrue(GroupManager.can_manage_groups(user))
def test_can_manage_groups_no_perm_leads_group(self):
user = AuthUtils.create_user('Clark Kent')
self.group_default.authgroup.group_leaders.add(user)
self.assertTrue(GroupManager.can_manage_groups(user))
def test_can_manage_group_no_perm_no_group(self):
user = AuthUtils.create_user('Clark Kent')
self.assertFalse(
GroupManager.can_manage_group(user, self.group_default)
)
def test_can_manage_group_has_perm(self):
user = AuthUtils.create_user('Clark Kent')
AuthUtils.add_permission_to_user_by_name(
'auth.group_management', user
)
self.assertTrue(
GroupManager.can_manage_group(user, self.group_default)
)
def test_can_manage_group_no_perm_leads_correct_group(self):
user = AuthUtils.create_user('Clark Kent')
self.group_default.authgroup.group_leaders.add(user)
self.assertTrue(
GroupManager.can_manage_group(user, self.group_default)
)
def test_can_manage_group_no_perm_leads_other_group(self):
user = AuthUtils.create_user('Clark Kent')
self.group_hidden.authgroup.group_leaders.add(user)
self.assertFalse(
GroupManager.can_manage_group(user, self.group_default)
)
def test_can_manage_group_user_not_authenticated(self):
user = MockUserNotAuthenticated()
self.assertFalse(
GroupManager.can_manage_group(user, self.group_default)
)

View File

@@ -0,0 +1,167 @@
from unittest import mock
from django.contrib.auth.models import User, Group
from django.test import TestCase
from allianceauth.tests.auth_utils import AuthUtils
from allianceauth.eveonline.models import (
EveCorporationInfo, EveAllianceInfo, EveCharacter
)
from ..models import GroupRequest, RequestLog
def create_testdata():
# clear DB
User.objects.all().delete()
Group.objects.all().delete()
EveCharacter.objects.all().delete()
EveCorporationInfo.objects.all().delete()
EveAllianceInfo.objects.all().delete()
# group 1
group = Group.objects.create(name='Superheros')
group.authgroup.description = 'Default Group'
group.authgroup.internal = False
group.authgroup.hidden = False
group.authgroup.save()
# user 1
user_1 = AuthUtils.create_user('Bruce Wayne')
AuthUtils.add_main_character_2(
user_1,
name='Bruce Wayne',
character_id=1001,
corp_id=2001,
corp_name='Wayne Technologies'
)
user_1.groups.add(group)
group.authgroup.group_leaders.add(user_1)
# user 2
user_2 = AuthUtils.create_user('Clark Kent')
AuthUtils.add_main_character_2(
user_2,
name='Clark Kent',
character_id=1002,
corp_id=2002,
corp_name='Wayne Technologies'
)
return group, user_1, user_2
class TestGroupRequest(TestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.group, cls.user_1, _ = create_testdata()
def test_main_char(self):
group_request = GroupRequest.objects.create(
status='Pending',
user=self.user_1,
group=self.group
)
expected = self.user_1.profile.main_character
self.assertEqual(group_request.main_char, expected)
def test_str(self):
group_request = GroupRequest.objects.create(
status='Pending',
user=self.user_1,
group=self.group
)
expected = 'Bruce Wayne:Superheros'
self.assertEqual(str(group_request), expected)
class TestRequestLog(TestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.group, cls.user_1, cls.user_2 = create_testdata()
def test_requestor(self):
request_log = RequestLog.objects.create(
group=self.group,
request_info='Clark Kent:Superheros',
request_actor=self.user_1
)
expected = 'Clark Kent'
self.assertEqual(request_log.requestor(), expected)
def test_type_to_str_removed(self):
request_log = RequestLog.objects.create(
request_type=None,
group=self.group,
request_info='Clark Kent:Superheros',
request_actor=self.user_1
)
expected = 'Removed'
self.assertEqual(request_log.type_to_str(), expected)
def test_type_to_str_leave(self):
request_log = RequestLog.objects.create(
request_type=True,
group=self.group,
request_info='Clark Kent:Superheros',
request_actor=self.user_1
)
expected = 'Leave'
self.assertEqual(request_log.type_to_str(), expected)
def test_type_to_str_join(self):
request_log = RequestLog.objects.create(
request_type=False,
group=self.group,
request_info='Clark Kent:Superheros',
request_actor=self.user_1
)
expected = 'Join'
self.assertEqual(request_log.type_to_str(), expected)
def test_action_to_str_accept(self):
request_log = RequestLog.objects.create(
group=self.group,
request_info='Clark Kent:Superheros',
request_actor=self.user_1,
action = True
)
expected = 'Accept'
self.assertEqual(request_log.action_to_str(), expected)
def test_action_to_str_reject(self):
request_log = RequestLog.objects.create(
group=self.group,
request_info='Clark Kent:Superheros',
request_actor=self.user_1,
action = False
)
expected = 'Reject'
self.assertEqual(request_log.action_to_str(), expected)
def test_req_char(self):
request_log = RequestLog.objects.create(
group=self.group,
request_info='Clark Kent:Superheros',
request_actor=self.user_1,
action = False
)
expected = self.user_2.profile.main_character
self.assertEqual(request_log.req_char(), expected)
class TestAuthGroup(TestCase):
def test_str(self):
group = Group.objects.create(name='Superheros')
group.authgroup.description = 'Default Group'
group.authgroup.internal = False
group.authgroup.hidden = False
group.authgroup.save()
expected = 'Superheros'
self.assertEqual(str(group.authgroup), expected)

View File

@@ -0,0 +1,61 @@
from unittest import mock
from django.test import TestCase
from django.contrib.auth.models import User, Group
from allianceauth.eveonline.models import EveCorporationInfo, EveAllianceInfo
from allianceauth.tests.auth_utils import AuthUtils
from ..signals import check_groups_on_state_change
class GroupManagementStateTestCase(TestCase):
@classmethod
def setUpTestData(cls):
cls.user = AuthUtils.create_user('test')
AuthUtils.add_main_character(
cls.user, 'test character', '1', corp_id='2', corp_name='test_corp', corp_ticker='TEST', alliance_id='3', alliance_name='TEST'
)
cls.user.profile.refresh_from_db()
cls.alliance = EveAllianceInfo.objects.create(
alliance_id='3', alliance_name='test alliance', alliance_ticker='TEST', executor_corp_id='2'
)
cls.corp = EveCorporationInfo.objects.create(
corporation_id='2', corporation_name='test corp', corporation_ticker='TEST', alliance=cls.alliance, member_count=1
)
cls.state_group = Group.objects.create(name='state_group')
cls.open_group = Group.objects.create(name='open_group')
cls.state = AuthUtils.create_state('test state', 500)
cls.state_group.authgroup.states.add(cls.state)
cls.state_group.authgroup.internal = False
cls.state_group.save()
def setUp(self):
self.user.refresh_from_db()
self.state.refresh_from_db()
def _refresh_user(self):
self.user = User.objects.get(pk=self.user.pk)
def _refresh_test_group(self):
self.state_group = Group.objects.get(pk=self.state_group.pk)
def test_drop_state_group(self):
self.user.groups.add(self.open_group)
self.user.groups.add(self.state_group)
self.assertEqual(self.user.profile.state.name, "Guest")
self.state.member_corporations.add(self.corp)
self._refresh_user()
self.assertEqual(self.user.profile.state, self.state)
groups = self.user.groups.all()
self.assertIn(self.state_group, groups) #keeps group
self.assertIn(self.open_group, groups) #public group unafected
self.state.member_corporations.clear()
self._refresh_user()
self.assertEqual(self.user.profile.state.name, "Guest")
groups = self.user.groups.all()
self.assertNotIn(self.state_group, groups) #looses group
self.assertIn(self.open_group, groups) #public group unafected

View File

@@ -0,0 +1,22 @@
from unittest.mock import Mock, patch
from django.test import RequestFactory, TestCase
from django.urls import reverse
from allianceauth.tests.auth_utils import AuthUtils
from esi.models import Token
from .. import views
class TestViews(TestCase):
def setUp(self):
self.factory = RequestFactory()
self.user = AuthUtils.create_user('Bruce Wayne')
def test_groups_view_can_load(self):
request = self.factory.get(reverse('groupmanagement:groups'))
request.user = self.user
response = views.groups_view(request)
self.assertEqual(response.status_code, 200)

View File

@@ -1,5 +1,6 @@
import logging
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.contrib.auth.decorators import user_passes_test
@@ -10,12 +11,12 @@ from django.db.models import Count
from django.http import Http404
from django.shortcuts import render, redirect, get_object_or_404
from django.utils.translation import ugettext_lazy as _
from .managers import GroupManager
from .models import GroupRequest, RequestLog
from allianceauth.notifications import notify
from django.conf import settings
from .managers import GroupManager
from .models import GroupRequest, RequestLog
logger = logging.getLogger(__name__)
@@ -235,7 +236,7 @@ def group_reject_request(request, group_request_id):
raise p
except:
messages.error(request, _('An unhandled error occurred while processing the application from %(mainchar)s to %(group)s.') % {"mainchar": group_request.main_char, "group": group_request.group})
logger.exception("Unhandled exception occured while user %s attempting to reject group request id %s" % (
logger.exception("Unhandled exception occurred while user %s attempting to reject group request id %s" % (
request.user, group_request_id))
pass
@@ -269,9 +270,9 @@ def group_leave_accept_request(request, group_request_id):
(request.user, group_request_id))
raise p
except:
messages.error(request, _('An unhandled error occured while processing the application from %(mainchar)s to leave %(group)s.') % {
messages.error(request, _('An unhandled error occurred while processing the application from %(mainchar)s to leave %(group)s.') % {
"mainchar": group_request.main_char, "group": group_request.group})
logger.exception("Unhandled exception occured while user %s attempting to accept group leave request id %s" % (
logger.exception("Unhandled exception occurred while user %s attempting to accept group leave request id %s" % (
request.user, group_request_id))
pass
@@ -303,9 +304,9 @@ def group_leave_reject_request(request, group_request_id):
(request.user, group_request_id))
raise p
except:
messages.error(request, _('An unhandled error occured while processing the application from %(mainchar)s to leave %(group)s.') % {
messages.error(request, _('An unhandled error occurred while processing the application from %(mainchar)s to leave %(group)s.') % {
"mainchar": group_request.main_char, "group": group_request.group})
logger.exception("Unhandled exception occured while user %s attempting to reject group leave request id %s" % (
logger.exception("Unhandled exception occurred while user %s attempting to reject group leave request id %s" % (
request.user, group_request_id))
pass
@@ -315,24 +316,23 @@ def group_leave_reject_request(request, group_request_id):
@login_required
def groups_view(request):
logger.debug("groups_view called by user %s" % request.user)
groups_qs = GroupManager.get_joinable_groups_for_user(
request.user, include_hidden=False
)
groups_qs = groups_qs.order_by('name')
groups = []
for group in groups_qs:
group_request = GroupRequest.objects\
.filter(user=request.user)\
.filter(group=group)
groups.append({
'group': group,
'request': group_request[0] if group_request else None
})
group_query = GroupManager.get_joinable_groups(request.user.profile.state)
if not request.user.has_perm('groupmanagement.request_groups'):
# Filter down to public groups only for non-members
group_query = group_query.filter(authgroup__public=True)
logger.debug("Not a member, only public groups will be available")
for group in group_query:
# Exclude hidden
if not group.authgroup.hidden:
group_request = GroupRequest.objects.filter(user=request.user).filter(group=group)
groups.append({'group': group, 'request': group_request[0] if group_request else None})
render_items = {'groups': groups}
return render(request, 'groupmanagement/groups.html', context=render_items)
context = {'groups': groups}
return render(request, 'groupmanagement/groups.html', context=context)
@login_required
@@ -349,13 +349,13 @@ def group_request_add(request, group_id):
# User is already a member of this group.
logger.warning("User %s attempted to join group id %s but they are already a member." %
(request.user, group_id))
messages.warning(request, "You are already a member of that group.")
messages.warning(request, _("You are already a member of that group."))
return redirect('groupmanagement:groups')
if not request.user.has_perm('groupmanagement.request_groups') and not group.authgroup.public:
# Does not have the required permission, trying to join a non-public group
logger.warning("User %s attempted to join group id %s but it is not a public group" %
(request.user, group_id))
messages.warning(request, "You cannot join that group")
messages.warning(request, _("You cannot join that group"))
return redirect('groupmanagement:groups')
if group.authgroup.open:
logger.info("%s joining %s as is an open group" % (request.user, group))
@@ -364,7 +364,7 @@ def group_request_add(request, group_id):
req = GroupRequest.objects.filter(user=request.user, group=group)
if len(req) > 0:
logger.info("%s attempted to join %s but already has an open application" % (request.user, group))
messages.warning(request, "You already have a pending application for that group.")
messages.warning(request, _("You already have a pending application for that group."))
return redirect("groupmanagement:groups")
grouprequest = GroupRequest()
grouprequest.status = _('Pending')
@@ -398,7 +398,7 @@ def group_request_leave(request, group_id):
req = GroupRequest.objects.filter(user=request.user, group=group)
if len(req) > 0:
logger.info("%s attempted to leave %s but already has an pending leave request." % (request.user, group))
messages.warning(request, "You already have a pending leave request for that group.")
messages.warning(request, _("You already have a pending leave request for that group."))
return redirect("groupmanagement:groups")
if getattr(settings, 'AUTO_LEAVE', False):
logger.info("%s leaving joinable group %s due to auto_leave" % (request.user, group))

View File

@@ -4,18 +4,18 @@
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# Rounon Dax <rounon.dax@terra-nanotech.de>, 2020
# Erik Kalkoken <erik.kalkoken@gmail.com>, 2020
# Joel Falknau <ozirascal@gmail.com>, 2020
# Rounon Dax <rounon.dax@terra-nanotech.de>, 2020
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-03-04 23:04+0000\n"
"POT-Creation-Date: 2020-04-02 03:23+0000\n"
"PO-Revision-Date: 2020-02-18 03:14+0000\n"
"Last-Translator: Joel Falknau <ozirascal@gmail.com>, 2020\n"
"Last-Translator: Rounon Dax <rounon.dax@terra-nanotech.de>, 2020\n"
"Language-Team: German (https://www.transifex.com/alliance-auth/teams/107430/de/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -71,9 +71,7 @@ msgid "Change Main"
msgstr "Hauptcharakter ändern"
#: allianceauth/authentication/templates/authentication/dashboard.html:97
#: allianceauth/permissions_tool/templates/permissions_tool/overview.html:40
#: allianceauth/templates/allianceauth/side-menu.html:15
msgid "Groups"
msgid "Group Memberships"
msgstr "Gruppen"
#: allianceauth/authentication/templates/authentication/dashboard.html:117
@@ -140,32 +138,41 @@ msgstr "Euer IT Team"
msgid "Submit"
msgstr "Absenden"
#: allianceauth/authentication/views.py:39
#: allianceauth/authentication/views.py:74
#, python-format
msgid ""
"Cannot change main character to %(char)s: character owned by a different "
"account."
msgstr ""
"Der Haputcharakter kann nicht zu %(char)s geändert werden. Dieser Charakter "
"gehört zu einem anderen Konto."
#: allianceauth/authentication/views.py:80
#, python-format
msgid "Changed main character to %(char)s"
msgstr "Haupcharakter geändert zu %(char)s"
#: allianceauth/authentication/views.py:48
#: allianceauth/authentication/views.py:89
#, python-format
msgid "Added %(name)s to your account."
msgstr "%(name)s zu Deinem Konto hinzugefügt."
#: allianceauth/authentication/views.py:50
#: allianceauth/authentication/views.py:91
#, python-format
msgid "Failed to add %(name)s to your account: they already have an account."
msgstr ""
"Es ist nicht möglich %(name)s zu Deinem Konto hinzu zu fügen: Dieser hat "
"bereits ein eigenes Konto."
#: allianceauth/authentication/views.py:89
#: allianceauth/authentication/views.py:130
msgid "Unable to authenticate as the selected character."
msgstr "Authentifizierung mit dem ausgewählten Charakter nicht möglich."
#: allianceauth/authentication/views.py:107
#: allianceauth/authentication/views.py:148
msgid "Registration token has expired."
msgstr "Token zur Registrierung ist abgelaufen."
#: allianceauth/authentication/views.py:159
#: allianceauth/authentication/views.py:200
msgid ""
"Sent confirmation email. Please follow the link to confirm your email "
"address."
@@ -173,12 +180,12 @@ msgstr ""
"Bestätigungsmail gesendet. Bitte folge dem Link in der E-Mail zur "
"Bestätigung."
#: allianceauth/authentication/views.py:164
#: allianceauth/authentication/views.py:205
msgid "Confirmed your email address. Please login to continue."
msgstr ""
"Deine E-Mail Adresse wurde bestätigt. Bitte log Dich ein um fortzufahren."
#: allianceauth/authentication/views.py:169
#: allianceauth/authentication/views.py:210
msgid "Registraion of new accounts it not allowed at this time."
msgstr "Registrierung von neuen Konten ist zur Zeit nicht erlaubt."
@@ -540,6 +547,12 @@ msgstr "FAT-Link ist abgelaufen."
msgid "Audit Log"
msgstr "Protokoll"
#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:18
#: allianceauth/groupmanagement/templates/groupmanagement/groupmembers.html:20
#: allianceauth/permissions_tool/templates/permissions_tool/audit.html:13
msgid "Back"
msgstr "Zurück"
#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:25
msgid "Date/Time"
msgstr "Datum/Uhrzeit"
@@ -599,6 +612,12 @@ msgstr "Keine Gruppenmitglieder vorhanden."
msgid "Groups Membership"
msgstr "Gruppenmitgliedschaft"
#: allianceauth/groupmanagement/templates/groupmanagement/groupmembership.html:14
#: allianceauth/permissions_tool/templates/permissions_tool/overview.html:40
#: allianceauth/templates/allianceauth/side-menu.html:15
msgid "Groups"
msgstr "Gruppen"
#: allianceauth/groupmanagement/templates/groupmanagement/groupmembership.html:23
#: allianceauth/groupmanagement/templates/groupmanagement/groups.html:16
msgid "Description"
@@ -718,26 +737,26 @@ msgstr "Gruppenanfragen"
msgid "Group Membership"
msgstr "Gruppenmitgliedschaft"
#: allianceauth/groupmanagement/views.py:164
#: allianceauth/groupmanagement/views.py:166
#, python-format
msgid "Removed user %(user)s from group %(group)s."
msgstr "Benutzer %(user)s von Gruppe %(group)s entfernt."
#: allianceauth/groupmanagement/views.py:166
#: allianceauth/groupmanagement/views.py:168
msgid "User does not exist in that group"
msgstr "Benutzer existiert nicht in dieser Gruppe"
#: allianceauth/groupmanagement/views.py:169
#: allianceauth/groupmanagement/views.py:171
msgid "Group does not exist"
msgstr "Gruppe existiert nicht"
#: allianceauth/groupmanagement/views.py:196
#: allianceauth/groupmanagement/views.py:198
#, python-format
msgid "Accepted application from %(mainchar)s to %(group)s."
msgstr "Beitrittsgesuch von %(mainchar)s zur Gruppe %(group)s zugestimmt."
#: allianceauth/groupmanagement/views.py:203
#: allianceauth/groupmanagement/views.py:236
#: allianceauth/groupmanagement/views.py:205
#: allianceauth/groupmanagement/views.py:238
#, python-format
msgid ""
"An unhandled error occurred while processing the application from "
@@ -746,37 +765,46 @@ msgstr ""
"Bei der Bearbeitung des Beitrittsgesuchs von %(mainchar)s zur Gruppe "
"%(group)s ist ein unbehandelter Fehler aufgetreten."
#: allianceauth/groupmanagement/views.py:229
#: allianceauth/groupmanagement/views.py:231
#, python-format
msgid "Rejected application from %(mainchar)s to %(group)s."
msgstr "Beitrittsgesuch von %(mainchar)s zur Gruppe %(group)s abgelehnt."
#: allianceauth/groupmanagement/views.py:265
#: allianceauth/groupmanagement/views.py:267
#, python-format
msgid "Accepted application from %(mainchar)s to leave %(group)s."
msgstr "Austrittsgesuch von %(mainchar)s für Gruppe %(group)s akzeptiert."
#: allianceauth/groupmanagement/views.py:271
#: allianceauth/groupmanagement/views.py:305
#: allianceauth/groupmanagement/views.py:273
#: allianceauth/groupmanagement/views.py:307
#, python-format
msgid ""
"An unhandled error occured while processing the application from "
"An unhandled error occurred while processing the application from "
"%(mainchar)s to leave %(group)s."
msgstr ""
"Bei der Bearbeitung des Austrittsgesuchs von %(mainchar)s für Gruppe "
"%(group)s ist ein unbehandelter Fehler aufgetreten."
#: allianceauth/groupmanagement/views.py:298
#: allianceauth/groupmanagement/views.py:300
#, python-format
msgid "Rejected application from %(mainchar)s to leave %(group)s."
msgstr "Austrittsgesuch von %(mainchar)s für Gruppe %(group)s abgelehnt."
#: allianceauth/groupmanagement/views.py:345
#: allianceauth/groupmanagement/views.py:346
#: allianceauth/groupmanagement/views.py:358
msgid "You cannot join that group"
msgstr "Du kannst dieser Gruppe nicht beitreten"
#: allianceauth/groupmanagement/views.py:369
#: allianceauth/groupmanagement/views.py:407
#: allianceauth/groupmanagement/views.py:352
msgid "You are already a member of that group."
msgstr "Du bist bereits Mitglied dieser Gruppe."
#: allianceauth/groupmanagement/views.py:367
msgid "You already have a pending application for that group."
msgstr "Du hast Dich bereits für diese Gruppe beworben."
#: allianceauth/groupmanagement/views.py:370
#: allianceauth/groupmanagement/views.py:408
#: allianceauth/hrapplications/templates/hrapplications/management.html:37
#: allianceauth/hrapplications/templates/hrapplications/management.html:72
#: allianceauth/hrapplications/templates/hrapplications/management.html:99
@@ -788,20 +816,24 @@ msgstr "Du kannst dieser Gruppe nicht beitreten"
msgid "Pending"
msgstr "Beantragt"
#: allianceauth/groupmanagement/views.py:375
#: allianceauth/groupmanagement/views.py:376
#, python-format
msgid "Applied to group %(group)s."
msgstr "Beitritt zur Gruppe %(group)s beantragt."
#: allianceauth/groupmanagement/views.py:386
#: allianceauth/groupmanagement/views.py:387
msgid "You cannot leave that group"
msgstr "Du kannst diese Gruppe nicht verlassen"
#: allianceauth/groupmanagement/views.py:391
#: allianceauth/groupmanagement/views.py:392
msgid "You are not a member of that group"
msgstr "Du bist kein Mitglied dieser Gruppe"
#: allianceauth/groupmanagement/views.py:413
#: allianceauth/groupmanagement/views.py:401
msgid "You already have a pending leave request for that group."
msgstr "Du hast bereits ein ausstehendes Austrittsgesuch für diese Gruppe."
#: allianceauth/groupmanagement/views.py:414
#, python-format
msgid "Applied to leave group %(group)s."
msgstr "Austrittsgesuch für Gruppe %(group)s gesendet."
@@ -1147,10 +1179,6 @@ msgstr "Änderungen für Operation timer %(opname)s gespeichert."
msgid "Permissions Audit"
msgstr "Berechtigungsprüfung"
#: allianceauth/permissions_tool/templates/permissions_tool/audit.html:13
msgid "Back"
msgstr "Zurück"
#: allianceauth/permissions_tool/templates/permissions_tool/audit.html:22
msgid "User / Character"
msgstr "Benutzer / Character"
@@ -1196,6 +1224,18 @@ msgstr "Nutzer"
msgid "States"
msgstr "Status"
#: allianceauth/services/abstract.py:72
msgid "That service account already exists"
msgstr "Dieses Dienstkonto existiert bereits"
#: allianceauth/services/abstract.py:104
msgid "Successfully set your {} password"
msgstr "Dein {} Passwort wurde erfolgreich gesetzt"
#: allianceauth/services/auth_hooks.py:11
msgid "Services"
msgstr "Dienste"
#: allianceauth/services/forms.py:6
msgid "Name of Fleet:"
msgstr "SRP Flotte erstellen:"
@@ -1260,15 +1300,85 @@ msgstr "Passwort muss mindestens 8 Zeichen lang sein"
msgid "Link Discord Server"
msgstr "Verbinde Discord Server"
#: allianceauth/services/modules/openfire/forms.py:7
msgid "Message"
msgstr "Nachricht"
#: allianceauth/services/modules/discord/views.py:26
msgid "Deactivated Discord account."
msgstr "Discord Konto deaktiviert."
#: allianceauth/services/modules/discord/views.py:29
#: allianceauth/services/modules/discord/views.py:41
#: allianceauth/services/modules/discord/views.py:65
msgid "An error occurred while processing your Discord account."
msgstr "Es gab einen Fehler bei der Verarbeitung Deines Discord Kontos."
#: allianceauth/services/modules/discord/views.py:62
msgid "Activated Discord account."
msgstr "Discord Konto aktiviert."
#: allianceauth/services/modules/discourse/views.py:37
msgid "You are not authorized to access Discourse."
msgstr "Du bist nicht autorisiert auf Discorse zuzugreifen."
#: allianceauth/services/modules/discourse/views.py:42
msgid "You must have a main character set to access Discourse."
msgstr ""
"Du musst einen Hauptcharakter gesetzt haben um auf Discourse zuzugreifen."
#: allianceauth/services/modules/discourse/views.py:52
msgid ""
"No SSO payload or signature. Please contact support if this problem "
"persists."
msgstr ""
"Keine SSO-Nutzdaten oder Signaturen. Bitte wenden Sie sich an den Support, "
"wenn das Problem weiterhin besteht."
#: allianceauth/services/modules/discourse/views.py:62
#: allianceauth/services/modules/discourse/views.py:70
msgid "Invalid payload. Please contact support if this problem persists."
msgstr ""
"Ungültige Nutzdaten. Bitte wenden Sie sich an den Support, wenn das Problem "
"weiterhin besteht."
#: allianceauth/services/modules/ips4/views.py:31
msgid "Activated IPSuite4 account."
msgstr "IP4Suite Konto aktiviert."
#: allianceauth/services/modules/ips4/views.py:40
#: allianceauth/services/modules/ips4/views.py:62
#: allianceauth/services/modules/ips4/views.py:83
#: allianceauth/services/modules/ips4/views.py:103
msgid "An error occurred while processing your IPSuite4 account."
msgstr "Es gab einen Fehler bei der Verarbeitung Deines IPSuite4 Kontos."
#: allianceauth/services/modules/ips4/views.py:53
msgid "Reset IPSuite4 password."
msgstr "IPSuite4 Passwort zurücksetzen."
#: allianceauth/services/modules/ips4/views.py:80
msgid "Set IPSuite4 password."
msgstr "Setze IPSuite4 Passwort."
#: allianceauth/services/modules/ips4/views.py:100
msgid "Deactivated IPSuite4 account."
msgstr "IP4Suite Konto deaktiviert."
#: allianceauth/services/modules/openfire/auth_hooks.py:26
msgid "Jabber"
msgstr "Jabber"
#: allianceauth/services/modules/openfire/auth_hooks.py:78
#: allianceauth/services/modules/openfire/templates/services/openfire/broadcast.html:6
#: allianceauth/services/modules/openfire/templates/services/openfire/broadcast.html:11
msgid "Jabber Broadcast"
msgstr "Jabber Übertragung"
#: allianceauth/services/modules/openfire/auth_hooks.py:91
msgid "Fleet Broadcast Formatter"
msgstr "Flotten Ping Formatierung"
#: allianceauth/services/modules/openfire/forms.py:7
msgid "Message"
msgstr "Nachricht"
#: allianceauth/services/modules/openfire/templates/services/openfire/broadcast.html:17
msgid "Broadcast Sent!!"
msgstr "Übertragung gesendet!!"
@@ -1277,6 +1387,76 @@ msgstr "Übertragung gesendet!!"
msgid "Broadcast"
msgstr "Übertragungen"
#: allianceauth/services/modules/openfire/views.py:35
msgid "Activated jabber account."
msgstr "Jabber Konto aktiviert."
#: allianceauth/services/modules/openfire/views.py:44
#: allianceauth/services/modules/openfire/views.py:57
#: allianceauth/services/modules/openfire/views.py:78
#: allianceauth/services/modules/openfire/views.py:151
msgid "An error occurred while processing your jabber account."
msgstr "Es gab einen Fehler bei der Verarbeitung Deines Jabber Kontos."
#: allianceauth/services/modules/openfire/views.py:70
msgid "Reset jabber password."
msgstr "Jabber Passwort zurücksetzen."
#: allianceauth/services/modules/openfire/views.py:119
#, python-format
msgid "Sent jabber broadcast to %s"
msgstr "Sende Jabber Durchsage an %s"
#: allianceauth/services/modules/openfire/views.py:148
msgid "Set jabber password."
msgstr "Setze Jabber Passwort."
#: allianceauth/services/modules/phpbb3/views.py:34
msgid "Activated forum account."
msgstr "Forum Konto aktiviert."
#: allianceauth/services/modules/phpbb3/views.py:43
#: allianceauth/services/modules/phpbb3/views.py:57
#: allianceauth/services/modules/phpbb3/views.py:80
#: allianceauth/services/modules/phpbb3/views.py:103
msgid "An error occurred while processing your forum account."
msgstr "Es gab einen Fehler bei der Verarbeitung Deines Forum Kontos."
#: allianceauth/services/modules/phpbb3/views.py:54
msgid "Deactivated forum account."
msgstr "Forum Konto deaktiviert."
#: allianceauth/services/modules/phpbb3/views.py:71
msgid "Reset forum password."
msgstr "Forum Passwort zurücksetzen."
#: allianceauth/services/modules/phpbb3/views.py:100
msgid "Set forum password."
msgstr "Setze Forum Passwort."
#: allianceauth/services/modules/smf/views.py:34
msgid "Activated SMF account."
msgstr "SMF Konto aktiviert."
#: allianceauth/services/modules/smf/views.py:43
#: allianceauth/services/modules/smf/views.py:58
#: allianceauth/services/modules/smf/views.py:80
#: allianceauth/services/modules/smf/views.py:103
msgid "An error occurred while processing your SMF account."
msgstr "Es gab einen Fehler bei der Verarbeitung Deines SMF Kontos."
#: allianceauth/services/modules/smf/views.py:55
msgid "Deactivated SMF account."
msgstr "SMF Konto deaktiviert."
#: allianceauth/services/modules/smf/views.py:72
msgid "Reset SMF password."
msgstr "SMF Passwort zurücksetzen."
#: allianceauth/services/modules/smf/views.py:100
msgid "Set SMF password."
msgstr "Setze SMF Passwort."
#: allianceauth/services/modules/teamspeak3/forms.py:14
#, python-format
msgid "Unable to locate user %s on server"
@@ -1300,6 +1480,47 @@ msgstr "Server beitreten"
msgid "Continue"
msgstr "Fortsetzen"
#: allianceauth/services/modules/teamspeak3/views.py:34
msgid "Activated TeamSpeak3 account."
msgstr "TeamSpeak3 Konto aktiviert."
#: allianceauth/services/modules/teamspeak3/views.py:37
#: allianceauth/services/modules/teamspeak3/views.py:74
#: allianceauth/services/modules/teamspeak3/views.py:100
msgid "An error occurred while processing your TeamSpeak3 account."
msgstr "Es gab einen Fehler bei der Verarbeitung Deines TeamSpeak3 Kontos."
#: allianceauth/services/modules/teamspeak3/views.py:71
msgid "Deactivated TeamSpeak3 account."
msgstr "TeamSpeak3 Konto deaktiviert."
#: allianceauth/services/modules/teamspeak3/views.py:97
msgid "Reset TeamSpeak3 permission key."
msgstr "TeamSpeak3 Berechtigungsschlüssel zurücksetzen."
#: allianceauth/services/modules/xenforo/views.py:30
msgid "Activated XenForo account."
msgstr "XenForo Konto aktiviert."
#: allianceauth/services/modules/xenforo/views.py:40
#: allianceauth/services/modules/xenforo/views.py:52
#: allianceauth/services/modules/xenforo/views.py:73
#: allianceauth/services/modules/xenforo/views.py:94
msgid "An error occurred while processing your XenForo account."
msgstr "Es gab einen Fehler bei der Verarbeitung Deines XenForo Kontos."
#: allianceauth/services/modules/xenforo/views.py:50
msgid "Deactivated XenForo account."
msgstr "XenForo Konto deaktiviert."
#: allianceauth/services/modules/xenforo/views.py:65
msgid "Reset XenForo account password."
msgstr "XenForo Passwort zurüclsetzen."
#: allianceauth/services/modules/xenforo/views.py:91
msgid "Changed XenForo password."
msgstr "Setze XenForo Passwort."
#: allianceauth/services/templates/services/fleetformattertool.html:6
msgid "Fleet Formatter Tool"
msgstr "Flottenformatierung"

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-03-09 15:16+0000\n"
"POT-Creation-Date: 2020-04-02 03:23+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -65,9 +65,7 @@ msgid "Change Main"
msgstr ""
#: allianceauth/authentication/templates/authentication/dashboard.html:97
#: allianceauth/permissions_tool/templates/permissions_tool/overview.html:40
#: allianceauth/templates/allianceauth/side-menu.html:15
msgid "Groups"
msgid "Group Memberships"
msgstr ""
#: allianceauth/authentication/templates/authentication/dashboard.html:117
@@ -132,40 +130,47 @@ msgstr ""
msgid "Submit"
msgstr ""
#: allianceauth/authentication/views.py:39
#: allianceauth/authentication/views.py:74
#, python-format
msgid ""
"Cannot change main character to %(char)s: character owned by a different "
"account."
msgstr ""
#: allianceauth/authentication/views.py:80
#, python-format
msgid "Changed main character to %(char)s"
msgstr ""
#: allianceauth/authentication/views.py:48
#: allianceauth/authentication/views.py:89
#, python-format
msgid "Added %(name)s to your account."
msgstr ""
#: allianceauth/authentication/views.py:50
#: allianceauth/authentication/views.py:91
#, python-format
msgid "Failed to add %(name)s to your account: they already have an account."
msgstr ""
#: allianceauth/authentication/views.py:89
#: allianceauth/authentication/views.py:130
msgid "Unable to authenticate as the selected character."
msgstr ""
#: allianceauth/authentication/views.py:107
#: allianceauth/authentication/views.py:148
msgid "Registration token has expired."
msgstr ""
#: allianceauth/authentication/views.py:159
#: allianceauth/authentication/views.py:200
msgid ""
"Sent confirmation email. Please follow the link to confirm your email "
"address."
msgstr ""
#: allianceauth/authentication/views.py:164
#: allianceauth/authentication/views.py:205
msgid "Confirmed your email address. Please login to continue."
msgstr ""
#: allianceauth/authentication/views.py:169
#: allianceauth/authentication/views.py:210
msgid "Registraion of new accounts it not allowed at this time."
msgstr ""
@@ -527,6 +532,12 @@ msgstr ""
msgid "Audit Log"
msgstr ""
#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:18
#: allianceauth/groupmanagement/templates/groupmanagement/groupmembers.html:20
#: allianceauth/permissions_tool/templates/permissions_tool/audit.html:13
msgid "Back"
msgstr ""
#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:25
msgid "Date/Time"
msgstr ""
@@ -586,6 +597,12 @@ msgstr ""
msgid "Groups Membership"
msgstr ""
#: allianceauth/groupmanagement/templates/groupmanagement/groupmembership.html:14
#: allianceauth/permissions_tool/templates/permissions_tool/overview.html:40
#: allianceauth/templates/allianceauth/side-menu.html:15
msgid "Groups"
msgstr ""
#: allianceauth/groupmanagement/templates/groupmanagement/groupmembership.html:23
#: allianceauth/groupmanagement/templates/groupmanagement/groups.html:16
msgid "Description"
@@ -705,61 +722,70 @@ msgstr ""
msgid "Group Membership"
msgstr ""
#: allianceauth/groupmanagement/views.py:164
#: allianceauth/groupmanagement/views.py:166
#, python-format
msgid "Removed user %(user)s from group %(group)s."
msgstr ""
#: allianceauth/groupmanagement/views.py:166
#: allianceauth/groupmanagement/views.py:168
msgid "User does not exist in that group"
msgstr ""
#: allianceauth/groupmanagement/views.py:169
#: allianceauth/groupmanagement/views.py:171
msgid "Group does not exist"
msgstr ""
#: allianceauth/groupmanagement/views.py:196
#: allianceauth/groupmanagement/views.py:198
#, python-format
msgid "Accepted application from %(mainchar)s to %(group)s."
msgstr ""
#: allianceauth/groupmanagement/views.py:203
#: allianceauth/groupmanagement/views.py:236
#: allianceauth/groupmanagement/views.py:205
#: allianceauth/groupmanagement/views.py:238
#, python-format
msgid ""
"An unhandled error occurred while processing the application from "
"%(mainchar)s to %(group)s."
msgstr ""
#: allianceauth/groupmanagement/views.py:229
#: allianceauth/groupmanagement/views.py:231
#, python-format
msgid "Rejected application from %(mainchar)s to %(group)s."
msgstr ""
#: allianceauth/groupmanagement/views.py:265
#: allianceauth/groupmanagement/views.py:267
#, python-format
msgid "Accepted application from %(mainchar)s to leave %(group)s."
msgstr ""
#: allianceauth/groupmanagement/views.py:271
#: allianceauth/groupmanagement/views.py:305
#: allianceauth/groupmanagement/views.py:273
#: allianceauth/groupmanagement/views.py:307
#, python-format
msgid ""
"An unhandled error occured while processing the application from "
"An unhandled error occurred while processing the application from "
"%(mainchar)s to leave %(group)s."
msgstr ""
#: allianceauth/groupmanagement/views.py:298
#: allianceauth/groupmanagement/views.py:300
#, python-format
msgid "Rejected application from %(mainchar)s to leave %(group)s."
msgstr ""
#: allianceauth/groupmanagement/views.py:345
#: allianceauth/groupmanagement/views.py:346
#: allianceauth/groupmanagement/views.py:358
msgid "You cannot join that group"
msgstr ""
#: allianceauth/groupmanagement/views.py:369
#: allianceauth/groupmanagement/views.py:407
#: allianceauth/groupmanagement/views.py:352
msgid "You are already a member of that group."
msgstr ""
#: allianceauth/groupmanagement/views.py:367
msgid "You already have a pending application for that group."
msgstr ""
#: allianceauth/groupmanagement/views.py:370
#: allianceauth/groupmanagement/views.py:408
#: allianceauth/hrapplications/templates/hrapplications/management.html:37
#: allianceauth/hrapplications/templates/hrapplications/management.html:72
#: allianceauth/hrapplications/templates/hrapplications/management.html:99
@@ -771,20 +797,24 @@ msgstr ""
msgid "Pending"
msgstr ""
#: allianceauth/groupmanagement/views.py:375
#: allianceauth/groupmanagement/views.py:376
#, python-format
msgid "Applied to group %(group)s."
msgstr ""
#: allianceauth/groupmanagement/views.py:386
#: allianceauth/groupmanagement/views.py:387
msgid "You cannot leave that group"
msgstr ""
#: allianceauth/groupmanagement/views.py:391
#: allianceauth/groupmanagement/views.py:392
msgid "You are not a member of that group"
msgstr ""
#: allianceauth/groupmanagement/views.py:413
#: allianceauth/groupmanagement/views.py:401
msgid "You already have a pending leave request for that group."
msgstr ""
#: allianceauth/groupmanagement/views.py:414
#, python-format
msgid "Applied to leave group %(group)s."
msgstr ""
@@ -1130,10 +1160,6 @@ msgstr ""
msgid "Permissions Audit"
msgstr ""
#: allianceauth/permissions_tool/templates/permissions_tool/audit.html:13
msgid "Back"
msgstr ""
#: allianceauth/permissions_tool/templates/permissions_tool/audit.html:22
msgid "User / Character"
msgstr ""
@@ -1179,6 +1205,18 @@ msgstr ""
msgid "States"
msgstr ""
#: allianceauth/services/abstract.py:72
msgid "That service account already exists"
msgstr ""
#: allianceauth/services/abstract.py:104
msgid "Successfully set your {} password"
msgstr ""
#: allianceauth/services/auth_hooks.py:11
msgid "Services"
msgstr ""
#: allianceauth/services/forms.py:6
msgid "Name of Fleet:"
msgstr ""
@@ -1243,15 +1281,79 @@ msgstr ""
msgid "Link Discord Server"
msgstr ""
#: allianceauth/services/modules/openfire/forms.py:7
msgid "Message"
#: allianceauth/services/modules/discord/views.py:26
msgid "Deactivated Discord account."
msgstr ""
#: allianceauth/services/modules/discord/views.py:29
#: allianceauth/services/modules/discord/views.py:41
#: allianceauth/services/modules/discord/views.py:65
msgid "An error occurred while processing your Discord account."
msgstr ""
#: allianceauth/services/modules/discord/views.py:62
msgid "Activated Discord account."
msgstr ""
#: allianceauth/services/modules/discourse/views.py:37
msgid "You are not authorized to access Discourse."
msgstr ""
#: allianceauth/services/modules/discourse/views.py:42
msgid "You must have a main character set to access Discourse."
msgstr ""
#: allianceauth/services/modules/discourse/views.py:52
msgid ""
"No SSO payload or signature. Please contact support if this problem persists."
msgstr ""
#: allianceauth/services/modules/discourse/views.py:62
#: allianceauth/services/modules/discourse/views.py:70
msgid "Invalid payload. Please contact support if this problem persists."
msgstr ""
#: allianceauth/services/modules/ips4/views.py:31
msgid "Activated IPSuite4 account."
msgstr ""
#: allianceauth/services/modules/ips4/views.py:40
#: allianceauth/services/modules/ips4/views.py:62
#: allianceauth/services/modules/ips4/views.py:83
#: allianceauth/services/modules/ips4/views.py:103
msgid "An error occurred while processing your IPSuite4 account."
msgstr ""
#: allianceauth/services/modules/ips4/views.py:53
msgid "Reset IPSuite4 password."
msgstr ""
#: allianceauth/services/modules/ips4/views.py:80
msgid "Set IPSuite4 password."
msgstr ""
#: allianceauth/services/modules/ips4/views.py:100
msgid "Deactivated IPSuite4 account."
msgstr ""
#: allianceauth/services/modules/openfire/auth_hooks.py:26
msgid "Jabber"
msgstr ""
#: allianceauth/services/modules/openfire/auth_hooks.py:78
#: allianceauth/services/modules/openfire/templates/services/openfire/broadcast.html:6
#: allianceauth/services/modules/openfire/templates/services/openfire/broadcast.html:11
msgid "Jabber Broadcast"
msgstr ""
#: allianceauth/services/modules/openfire/auth_hooks.py:91
msgid "Fleet Broadcast Formatter"
msgstr ""
#: allianceauth/services/modules/openfire/forms.py:7
msgid "Message"
msgstr ""
#: allianceauth/services/modules/openfire/templates/services/openfire/broadcast.html:17
msgid "Broadcast Sent!!"
msgstr ""
@@ -1260,6 +1362,76 @@ msgstr ""
msgid "Broadcast"
msgstr ""
#: allianceauth/services/modules/openfire/views.py:35
msgid "Activated jabber account."
msgstr ""
#: allianceauth/services/modules/openfire/views.py:44
#: allianceauth/services/modules/openfire/views.py:57
#: allianceauth/services/modules/openfire/views.py:78
#: allianceauth/services/modules/openfire/views.py:151
msgid "An error occurred while processing your jabber account."
msgstr ""
#: allianceauth/services/modules/openfire/views.py:70
msgid "Reset jabber password."
msgstr ""
#: allianceauth/services/modules/openfire/views.py:119
#, python-format
msgid "Sent jabber broadcast to %s"
msgstr ""
#: allianceauth/services/modules/openfire/views.py:148
msgid "Set jabber password."
msgstr ""
#: allianceauth/services/modules/phpbb3/views.py:34
msgid "Activated forum account."
msgstr ""
#: allianceauth/services/modules/phpbb3/views.py:43
#: allianceauth/services/modules/phpbb3/views.py:57
#: allianceauth/services/modules/phpbb3/views.py:80
#: allianceauth/services/modules/phpbb3/views.py:103
msgid "An error occurred while processing your forum account."
msgstr ""
#: allianceauth/services/modules/phpbb3/views.py:54
msgid "Deactivated forum account."
msgstr ""
#: allianceauth/services/modules/phpbb3/views.py:71
msgid "Reset forum password."
msgstr ""
#: allianceauth/services/modules/phpbb3/views.py:100
msgid "Set forum password."
msgstr ""
#: allianceauth/services/modules/smf/views.py:34
msgid "Activated SMF account."
msgstr ""
#: allianceauth/services/modules/smf/views.py:43
#: allianceauth/services/modules/smf/views.py:58
#: allianceauth/services/modules/smf/views.py:80
#: allianceauth/services/modules/smf/views.py:103
msgid "An error occurred while processing your SMF account."
msgstr ""
#: allianceauth/services/modules/smf/views.py:55
msgid "Deactivated SMF account."
msgstr ""
#: allianceauth/services/modules/smf/views.py:72
msgid "Reset SMF password."
msgstr ""
#: allianceauth/services/modules/smf/views.py:100
msgid "Set SMF password."
msgstr ""
#: allianceauth/services/modules/teamspeak3/forms.py:14
#, python-format
msgid "Unable to locate user %s on server"
@@ -1283,6 +1455,47 @@ msgstr ""
msgid "Continue"
msgstr ""
#: allianceauth/services/modules/teamspeak3/views.py:34
msgid "Activated TeamSpeak3 account."
msgstr ""
#: allianceauth/services/modules/teamspeak3/views.py:37
#: allianceauth/services/modules/teamspeak3/views.py:74
#: allianceauth/services/modules/teamspeak3/views.py:100
msgid "An error occurred while processing your TeamSpeak3 account."
msgstr ""
#: allianceauth/services/modules/teamspeak3/views.py:71
msgid "Deactivated TeamSpeak3 account."
msgstr ""
#: allianceauth/services/modules/teamspeak3/views.py:97
msgid "Reset TeamSpeak3 permission key."
msgstr ""
#: allianceauth/services/modules/xenforo/views.py:30
msgid "Activated XenForo account."
msgstr ""
#: allianceauth/services/modules/xenforo/views.py:40
#: allianceauth/services/modules/xenforo/views.py:52
#: allianceauth/services/modules/xenforo/views.py:73
#: allianceauth/services/modules/xenforo/views.py:94
msgid "An error occurred while processing your XenForo account."
msgstr ""
#: allianceauth/services/modules/xenforo/views.py:50
msgid "Deactivated XenForo account."
msgstr ""
#: allianceauth/services/modules/xenforo/views.py:65
msgid "Reset XenForo account password."
msgstr ""
#: allianceauth/services/modules/xenforo/views.py:91
msgid "Changed XenForo password."
msgstr ""
#: allianceauth/services/templates/services/fleetformattertool.html:6
msgid "Fleet Formatter Tool"
msgstr ""

View File

@@ -12,7 +12,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-03-04 23:04+0000\n"
"POT-Creation-Date: 2020-03-26 03:07+0000\n"
"PO-Revision-Date: 2020-02-18 03:14+0000\n"
"Last-Translator: frank1210 <francolopez_16@hotmail.com>, 2020\n"
"Language-Team: Spanish (https://www.transifex.com/alliance-auth/teams/107430/es/)\n"
@@ -70,10 +70,8 @@ msgid "Change Main"
msgstr "Cambiar Personaje Principal"
#: allianceauth/authentication/templates/authentication/dashboard.html:97
#: allianceauth/permissions_tool/templates/permissions_tool/overview.html:40
#: allianceauth/templates/allianceauth/side-menu.html:15
msgid "Groups"
msgstr "Grupos"
msgid "Group Memberships"
msgstr ""
#: allianceauth/authentication/templates/authentication/dashboard.html:117
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticscorpview.html:23
@@ -137,45 +135,52 @@ msgstr "Tu equipo de IT"
msgid "Submit"
msgstr "Enviar"
#: allianceauth/authentication/views.py:39
#: allianceauth/authentication/views.py:74
#, python-format
msgid ""
"Cannot change main character to %(char)s: character owned by a different "
"account."
msgstr ""
#: allianceauth/authentication/views.py:80
#, python-format
msgid "Changed main character to %(char)s"
msgstr "Se ha cambiado tu personaje principal ha %(char)s"
#: allianceauth/authentication/views.py:48
#: allianceauth/authentication/views.py:89
#, python-format
msgid "Added %(name)s to your account."
msgstr "Se ha agregado a %(name)s a tu cuenta"
#: allianceauth/authentication/views.py:50
#: allianceauth/authentication/views.py:91
#, python-format
msgid "Failed to add %(name)s to your account: they already have an account."
msgstr ""
"Se fallo en agregar a %(name)s a tu cuenta: Ya se encuentra registrado en "
"otra cuenta."
#: allianceauth/authentication/views.py:89
#: allianceauth/authentication/views.py:130
msgid "Unable to authenticate as the selected character."
msgstr "Imposible validar con el personaje seleccionado."
#: allianceauth/authentication/views.py:107
#: allianceauth/authentication/views.py:148
msgid "Registration token has expired."
msgstr "El token de registracion expiro."
#: allianceauth/authentication/views.py:159
#: allianceauth/authentication/views.py:200
msgid ""
"Sent confirmation email. Please follow the link to confirm your email "
"address."
msgstr ""
"Confirmacion de mail enviada. Por favor siga el enlace para confirmar "
#: allianceauth/authentication/views.py:164
#: allianceauth/authentication/views.py:205
msgid "Confirmed your email address. Please login to continue."
msgstr ""
"Se ha confirmado su direccion de mail. Por favor igrese su token para "
"continuar."
#: allianceauth/authentication/views.py:169
#: allianceauth/authentication/views.py:210
msgid "Registraion of new accounts it not allowed at this time."
msgstr "Registracion de nuevas cuentas no es permitido por el momento."
@@ -538,6 +543,12 @@ msgstr "Enlace de participacion expirado."
msgid "Audit Log"
msgstr "Log de Auditoria"
#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:18
#: allianceauth/groupmanagement/templates/groupmanagement/groupmembers.html:20
#: allianceauth/permissions_tool/templates/permissions_tool/audit.html:13
msgid "Back"
msgstr "Volver"
#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:25
msgid "Date/Time"
msgstr "Fecha/Hora"
@@ -597,6 +608,12 @@ msgstr "no hay miembros para listar."
msgid "Groups Membership"
msgstr "Membresia de grupos"
#: allianceauth/groupmanagement/templates/groupmanagement/groupmembership.html:14
#: allianceauth/permissions_tool/templates/permissions_tool/overview.html:40
#: allianceauth/templates/allianceauth/side-menu.html:15
msgid "Groups"
msgstr "Grupos"
#: allianceauth/groupmanagement/templates/groupmanagement/groupmembership.html:23
#: allianceauth/groupmanagement/templates/groupmanagement/groups.html:16
msgid "Description"
@@ -716,26 +733,26 @@ msgstr "Solicitudes de Grupo"
msgid "Group Membership"
msgstr "Membresia de Grupo"
#: allianceauth/groupmanagement/views.py:164
#: allianceauth/groupmanagement/views.py:166
#, python-format
msgid "Removed user %(user)s from group %(group)s."
msgstr "El usuario %(user)s fue removido del grupo %(group)s"
#: allianceauth/groupmanagement/views.py:166
#: allianceauth/groupmanagement/views.py:168
msgid "User does not exist in that group"
msgstr "El usuario no existe en ese grupos"
#: allianceauth/groupmanagement/views.py:169
#: allianceauth/groupmanagement/views.py:171
msgid "Group does not exist"
msgstr "El grupo no existe"
#: allianceauth/groupmanagement/views.py:196
#: allianceauth/groupmanagement/views.py:198
#, python-format
msgid "Accepted application from %(mainchar)s to %(group)s."
msgstr "Solicitud aceptada de %(mainchar)s a %(group)s"
#: allianceauth/groupmanagement/views.py:203
#: allianceauth/groupmanagement/views.py:236
#: allianceauth/groupmanagement/views.py:205
#: allianceauth/groupmanagement/views.py:238
#, python-format
msgid ""
"An unhandled error occurred while processing the application from "
@@ -744,38 +761,45 @@ msgstr ""
"Ocurrio un error cuando se intento procesar la informacion de %(mainchar)s "
"al grupo %(group)s."
#: allianceauth/groupmanagement/views.py:229
#: allianceauth/groupmanagement/views.py:231
#, python-format
msgid "Rejected application from %(mainchar)s to %(group)s."
msgstr "Se rechazo la solicitud de %(mainchar)s al grupo %(group)s."
#: allianceauth/groupmanagement/views.py:265
#: allianceauth/groupmanagement/views.py:267
#, python-format
msgid "Accepted application from %(mainchar)s to leave %(group)s."
msgstr "Se acepto la solicitud de %(mainchar)s para dejar el grupo %(group)s."
#: allianceauth/groupmanagement/views.py:271
#: allianceauth/groupmanagement/views.py:305
#: allianceauth/groupmanagement/views.py:273
#: allianceauth/groupmanagement/views.py:307
#, python-format
msgid ""
"An unhandled error occured while processing the application from "
"An unhandled error occurred while processing the application from "
"%(mainchar)s to leave %(group)s."
msgstr ""
"Ocurrio un error cuando se intento procesar la informacion de %(mainchar)s "
"para dejar el grupo %(group)s."
#: allianceauth/groupmanagement/views.py:298
#: allianceauth/groupmanagement/views.py:300
#, python-format
msgid "Rejected application from %(mainchar)s to leave %(group)s."
msgstr ""
"Se rechazo la solicitud de %(mainchar)s para dejar el grupo %(group)s."
#: allianceauth/groupmanagement/views.py:345
#: allianceauth/groupmanagement/views.py:347
#: allianceauth/groupmanagement/views.py:359
msgid "You cannot join that group"
msgstr "No puedes unirte a ese grupo"
#: allianceauth/groupmanagement/views.py:369
#: allianceauth/groupmanagement/views.py:407
#: allianceauth/groupmanagement/views.py:353
msgid "You are already a member of that group."
msgstr ""
#: allianceauth/groupmanagement/views.py:368
msgid "You already have a pending application for that group."
msgstr ""
#: allianceauth/groupmanagement/views.py:371
#: allianceauth/groupmanagement/views.py:409
#: allianceauth/hrapplications/templates/hrapplications/management.html:37
#: allianceauth/hrapplications/templates/hrapplications/management.html:72
#: allianceauth/hrapplications/templates/hrapplications/management.html:99
@@ -787,20 +811,24 @@ msgstr "No puedes unirte a ese grupo"
msgid "Pending"
msgstr "Pendiente"
#: allianceauth/groupmanagement/views.py:375
#: allianceauth/groupmanagement/views.py:377
#, python-format
msgid "Applied to group %(group)s."
msgstr "Solicitud enviada al grupo %(group)s."
#: allianceauth/groupmanagement/views.py:386
#: allianceauth/groupmanagement/views.py:388
msgid "You cannot leave that group"
msgstr "No puedes dejar el grupos"
#: allianceauth/groupmanagement/views.py:391
#: allianceauth/groupmanagement/views.py:393
msgid "You are not a member of that group"
msgstr "No eres miembro de ese grupo"
#: allianceauth/groupmanagement/views.py:413
#: allianceauth/groupmanagement/views.py:402
msgid "You already have a pending leave request for that group."
msgstr ""
#: allianceauth/groupmanagement/views.py:415
#, python-format
msgid "Applied to leave group %(group)s."
msgstr "Solicitaste dejar el grupo %(group)s."
@@ -1146,10 +1174,6 @@ msgstr "Se guardaron los cambios para la operacion %(opname)s"
msgid "Permissions Audit"
msgstr "Auditar Permisos"
#: allianceauth/permissions_tool/templates/permissions_tool/audit.html:13
msgid "Back"
msgstr "Volver"
#: allianceauth/permissions_tool/templates/permissions_tool/audit.html:22
msgid "User / Character"
msgstr ""
@@ -1195,6 +1219,18 @@ msgstr "Usuarios"
msgid "States"
msgstr "Estados"
#: allianceauth/services/abstract.py:72
msgid "That service account already exists"
msgstr ""
#: allianceauth/services/abstract.py:104
msgid "Successfully set your {} password"
msgstr ""
#: allianceauth/services/auth_hooks.py:11
msgid "Services"
msgstr ""
#: allianceauth/services/forms.py:6
msgid "Name of Fleet:"
msgstr "Nombre de la flota:"
@@ -1259,15 +1295,80 @@ msgstr "La contraseña tiene que tener 8 caracteres de largo minimo"
msgid "Link Discord Server"
msgstr "Enlace a servidor de Discord"
#: allianceauth/services/modules/openfire/forms.py:7
msgid "Message"
msgstr "Mensaje"
#: allianceauth/services/modules/discord/views.py:26
msgid "Deactivated Discord account."
msgstr ""
#: allianceauth/services/modules/discord/views.py:29
#: allianceauth/services/modules/discord/views.py:41
#: allianceauth/services/modules/discord/views.py:65
msgid "An error occurred while processing your Discord account."
msgstr ""
#: allianceauth/services/modules/discord/views.py:62
msgid "Activated Discord account."
msgstr ""
#: allianceauth/services/modules/discourse/views.py:37
msgid "You are not authorized to access Discourse."
msgstr ""
#: allianceauth/services/modules/discourse/views.py:42
msgid "You must have a main character set to access Discourse."
msgstr ""
#: allianceauth/services/modules/discourse/views.py:52
msgid ""
"No SSO payload or signature. Please contact support if this problem "
"persists."
msgstr ""
#: allianceauth/services/modules/discourse/views.py:62
#: allianceauth/services/modules/discourse/views.py:70
msgid "Invalid payload. Please contact support if this problem persists."
msgstr ""
#: allianceauth/services/modules/ips4/views.py:31
msgid "Activated IPSuite4 account."
msgstr ""
#: allianceauth/services/modules/ips4/views.py:40
#: allianceauth/services/modules/ips4/views.py:62
#: allianceauth/services/modules/ips4/views.py:83
#: allianceauth/services/modules/ips4/views.py:103
msgid "An error occurred while processing your IPSuite4 account."
msgstr ""
#: allianceauth/services/modules/ips4/views.py:53
msgid "Reset IPSuite4 password."
msgstr ""
#: allianceauth/services/modules/ips4/views.py:80
msgid "Set IPSuite4 password."
msgstr ""
#: allianceauth/services/modules/ips4/views.py:100
msgid "Deactivated IPSuite4 account."
msgstr ""
#: allianceauth/services/modules/openfire/auth_hooks.py:26
msgid "Jabber"
msgstr ""
#: allianceauth/services/modules/openfire/auth_hooks.py:78
#: allianceauth/services/modules/openfire/templates/services/openfire/broadcast.html:6
#: allianceauth/services/modules/openfire/templates/services/openfire/broadcast.html:11
msgid "Jabber Broadcast"
msgstr "Anuncio por Jabber"
#: allianceauth/services/modules/openfire/auth_hooks.py:91
msgid "Fleet Broadcast Formatter"
msgstr ""
#: allianceauth/services/modules/openfire/forms.py:7
msgid "Message"
msgstr "Mensaje"
#: allianceauth/services/modules/openfire/templates/services/openfire/broadcast.html:17
msgid "Broadcast Sent!!"
msgstr "Ping Enviado"
@@ -1276,6 +1377,76 @@ msgstr "Ping Enviado"
msgid "Broadcast"
msgstr "Anuncio"
#: allianceauth/services/modules/openfire/views.py:35
msgid "Activated jabber account."
msgstr ""
#: allianceauth/services/modules/openfire/views.py:44
#: allianceauth/services/modules/openfire/views.py:57
#: allianceauth/services/modules/openfire/views.py:78
#: allianceauth/services/modules/openfire/views.py:151
msgid "An error occurred while processing your jabber account."
msgstr ""
#: allianceauth/services/modules/openfire/views.py:70
msgid "Reset jabber password."
msgstr ""
#: allianceauth/services/modules/openfire/views.py:119
#, python-format
msgid "Sent jabber broadcast to %s"
msgstr ""
#: allianceauth/services/modules/openfire/views.py:148
msgid "Set jabber password."
msgstr ""
#: allianceauth/services/modules/phpbb3/views.py:34
msgid "Activated forum account."
msgstr ""
#: allianceauth/services/modules/phpbb3/views.py:43
#: allianceauth/services/modules/phpbb3/views.py:57
#: allianceauth/services/modules/phpbb3/views.py:80
#: allianceauth/services/modules/phpbb3/views.py:103
msgid "An error occurred while processing your forum account."
msgstr ""
#: allianceauth/services/modules/phpbb3/views.py:54
msgid "Deactivated forum account."
msgstr ""
#: allianceauth/services/modules/phpbb3/views.py:71
msgid "Reset forum password."
msgstr ""
#: allianceauth/services/modules/phpbb3/views.py:100
msgid "Set forum password."
msgstr ""
#: allianceauth/services/modules/smf/views.py:34
msgid "Activated SMF account."
msgstr ""
#: allianceauth/services/modules/smf/views.py:43
#: allianceauth/services/modules/smf/views.py:58
#: allianceauth/services/modules/smf/views.py:80
#: allianceauth/services/modules/smf/views.py:103
msgid "An error occurred while processing your SMF account."
msgstr ""
#: allianceauth/services/modules/smf/views.py:55
msgid "Deactivated SMF account."
msgstr ""
#: allianceauth/services/modules/smf/views.py:72
msgid "Reset SMF password."
msgstr ""
#: allianceauth/services/modules/smf/views.py:100
msgid "Set SMF password."
msgstr ""
#: allianceauth/services/modules/teamspeak3/forms.py:14
#, python-format
msgid "Unable to locate user %s on server"
@@ -1299,6 +1470,47 @@ msgstr "Unirse al Servidor"
msgid "Continue"
msgstr "Continuar"
#: allianceauth/services/modules/teamspeak3/views.py:34
msgid "Activated TeamSpeak3 account."
msgstr ""
#: allianceauth/services/modules/teamspeak3/views.py:37
#: allianceauth/services/modules/teamspeak3/views.py:74
#: allianceauth/services/modules/teamspeak3/views.py:100
msgid "An error occurred while processing your TeamSpeak3 account."
msgstr ""
#: allianceauth/services/modules/teamspeak3/views.py:71
msgid "Deactivated TeamSpeak3 account."
msgstr ""
#: allianceauth/services/modules/teamspeak3/views.py:97
msgid "Reset TeamSpeak3 permission key."
msgstr ""
#: allianceauth/services/modules/xenforo/views.py:30
msgid "Activated XenForo account."
msgstr ""
#: allianceauth/services/modules/xenforo/views.py:40
#: allianceauth/services/modules/xenforo/views.py:52
#: allianceauth/services/modules/xenforo/views.py:73
#: allianceauth/services/modules/xenforo/views.py:94
msgid "An error occurred while processing your XenForo account."
msgstr ""
#: allianceauth/services/modules/xenforo/views.py:50
msgid "Deactivated XenForo account."
msgstr ""
#: allianceauth/services/modules/xenforo/views.py:65
msgid "Reset XenForo account password."
msgstr ""
#: allianceauth/services/modules/xenforo/views.py:91
msgid "Changed XenForo password."
msgstr ""
#: allianceauth/services/templates/services/fleetformattertool.html:6
msgid "Fleet Formatter Tool"
msgstr "Formato de Ping"

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -5,15 +5,16 @@
#
# Translators:
# Joel Falknau <ozirascal@gmail.com>, 2020
# Jesse . <sgeine@hotmail.com>, 2020
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-03-04 23:04+0000\n"
"POT-Creation-Date: 2020-03-10 01:32+0000\n"
"PO-Revision-Date: 2020-02-18 03:14+0000\n"
"Last-Translator: Joel Falknau <ozirascal@gmail.com>, 2020\n"
"Last-Translator: Jesse . <sgeine@hotmail.com>, 2020\n"
"Language-Team: Chinese Simplified (https://www.transifex.com/alliance-auth/teams/107430/zh-Hans/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -68,10 +69,8 @@ msgid "Change Main"
msgstr "修改主要角色"
#: allianceauth/authentication/templates/authentication/dashboard.html:97
#: allianceauth/permissions_tool/templates/permissions_tool/overview.html:40
#: allianceauth/templates/allianceauth/side-menu.html:15
msgid "Groups"
msgstr "群组"
msgid "Group Memberships"
msgstr "用户组成员"
#: allianceauth/authentication/templates/authentication/dashboard.html:117
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticscorpview.html:23
@@ -135,40 +134,40 @@ msgstr "您的IT团队"
msgid "Submit"
msgstr "提交"
#: allianceauth/authentication/views.py:39
#: allianceauth/authentication/views.py:77
#, python-format
msgid "Changed main character to %(char)s"
msgstr "修改主要角色为%(char)s"
#: allianceauth/authentication/views.py:48
#: allianceauth/authentication/views.py:86
#, python-format
msgid "Added %(name)s to your account."
msgstr "添加%(name)s到您的账户"
#: allianceauth/authentication/views.py:50
#: allianceauth/authentication/views.py:88
#, python-format
msgid "Failed to add %(name)s to your account: they already have an account."
msgstr "添加%(name)s到您的账户失败他们已经在一个账户中了"
#: allianceauth/authentication/views.py:89
#: allianceauth/authentication/views.py:127
msgid "Unable to authenticate as the selected character."
msgstr "无法作为选定的角色进行身份验证"
#: allianceauth/authentication/views.py:107
#: allianceauth/authentication/views.py:145
msgid "Registration token has expired."
msgstr "注册令牌过期。"
#: allianceauth/authentication/views.py:159
#: allianceauth/authentication/views.py:197
msgid ""
"Sent confirmation email. Please follow the link to confirm your email "
"address."
msgstr "已经发送了确认邮件。请按照链接确定您的电邮地址"
#: allianceauth/authentication/views.py:164
#: allianceauth/authentication/views.py:202
msgid "Confirmed your email address. Please login to continue."
msgstr "已确认您的电邮地址。请登录以继续"
#: allianceauth/authentication/views.py:169
#: allianceauth/authentication/views.py:207
msgid "Registraion of new accounts it not allowed at this time."
msgstr "现在不允许注册新账户。"
@@ -706,61 +705,61 @@ msgstr "用户组请求"
msgid "Group Membership"
msgstr "用户组成员"
#: allianceauth/groupmanagement/views.py:164
#: allianceauth/groupmanagement/views.py:165
#, python-format
msgid "Removed user %(user)s from group %(group)s."
msgstr "已将用户%(user)s从用户组%(group)s中移除"
#: allianceauth/groupmanagement/views.py:166
#: allianceauth/groupmanagement/views.py:167
msgid "User does not exist in that group"
msgstr "那个用户组中不存在这个用户"
#: allianceauth/groupmanagement/views.py:169
#: allianceauth/groupmanagement/views.py:170
msgid "Group does not exist"
msgstr "用户组不存在"
#: allianceauth/groupmanagement/views.py:196
#: allianceauth/groupmanagement/views.py:197
#, python-format
msgid "Accepted application from %(mainchar)s to %(group)s."
msgstr "已接受用户%(mainchar)s加入%(group)s的申请"
#: allianceauth/groupmanagement/views.py:203
#: allianceauth/groupmanagement/views.py:236
#: allianceauth/groupmanagement/views.py:204
#: allianceauth/groupmanagement/views.py:237
#, python-format
msgid ""
"An unhandled error occurred while processing the application from "
"%(mainchar)s to %(group)s."
msgstr "在处理用户%(mainchar)s加入%(group)s的申请的过程中出现了一个搞不定的错误"
#: allianceauth/groupmanagement/views.py:229
#: allianceauth/groupmanagement/views.py:230
#, python-format
msgid "Rejected application from %(mainchar)s to %(group)s."
msgstr "%(mainchar)s加入%(group)s的申请已拒绝"
#: allianceauth/groupmanagement/views.py:265
#: allianceauth/groupmanagement/views.py:266
#, python-format
msgid "Accepted application from %(mainchar)s to leave %(group)s."
msgstr "%(mainchar)s加入%(group)s的申请已通过"
#: allianceauth/groupmanagement/views.py:271
#: allianceauth/groupmanagement/views.py:305
#: allianceauth/groupmanagement/views.py:272
#: allianceauth/groupmanagement/views.py:306
#, python-format
msgid ""
"An unhandled error occured while processing the application from "
"%(mainchar)s to leave %(group)s."
msgstr "在处理%(mainchar)s离开%(group)s的请求时发生了搞不定的错误"
#: allianceauth/groupmanagement/views.py:298
#: allianceauth/groupmanagement/views.py:299
#, python-format
msgid "Rejected application from %(mainchar)s to leave %(group)s."
msgstr "%(mainchar)s离开%(group)s的请求已被拒绝"
#: allianceauth/groupmanagement/views.py:345
#: allianceauth/groupmanagement/views.py:346
msgid "You cannot join that group"
msgstr "你无法加入那个用户组"
#: allianceauth/groupmanagement/views.py:369
#: allianceauth/groupmanagement/views.py:407
#: allianceauth/groupmanagement/views.py:370
#: allianceauth/groupmanagement/views.py:408
#: allianceauth/hrapplications/templates/hrapplications/management.html:37
#: allianceauth/hrapplications/templates/hrapplications/management.html:72
#: allianceauth/hrapplications/templates/hrapplications/management.html:99
@@ -772,20 +771,20 @@ msgstr "你无法加入那个用户组"
msgid "Pending"
msgstr "待定"
#: allianceauth/groupmanagement/views.py:375
#: allianceauth/groupmanagement/views.py:376
#, python-format
msgid "Applied to group %(group)s."
msgstr "修改已经应用到%(group)s啦"
#: allianceauth/groupmanagement/views.py:386
#: allianceauth/groupmanagement/views.py:387
msgid "You cannot leave that group"
msgstr "你无法离开那个用户组"
#: allianceauth/groupmanagement/views.py:391
#: allianceauth/groupmanagement/views.py:392
msgid "You are not a member of that group"
msgstr "你不是那个用户组的成员"
#: allianceauth/groupmanagement/views.py:413
#: allianceauth/groupmanagement/views.py:414
#, python-format
msgid "Applied to leave group %(group)s."
msgstr "已经离开群组%(group)s"
@@ -1176,6 +1175,11 @@ msgstr "操作类型"
msgid "Users"
msgstr "用户"
#: allianceauth/permissions_tool/templates/permissions_tool/overview.html:40
#: allianceauth/templates/allianceauth/side-menu.html:15
msgid "Groups"
msgstr "群组"
#: allianceauth/permissions_tool/templates/permissions_tool/overview.html:43
msgid "States"
msgstr "声望"

View File

@@ -11,6 +11,15 @@ app = Celery('{{ project_name }}')
# Using a string here means the worker don't have to serialize
# the configuration object to child processes.
app.config_from_object('django.conf:settings')
# setup priorities ( 0 Highest, 9 Lowest )
app.conf.broker_transport_options = {
'priority_steps': list(range(10)), # setup que to have 10 steps
'queue_order_strategy': 'priority', # setup que to use prio sorting
}
app.conf.task_default_priority = 5 # anything called with the task.delay() will be given normal priority (5)
app.conf.worker_prefetch_multiplier = 1 # only prefetch single tasks at a time on the workers so that prio tasks happen
app.conf.ONCE = {
'backend': 'allianceauth.services.tasks.DjangoBackend',
'settings': {}

View File

@@ -84,6 +84,8 @@ LANGUAGES = (
('de', ugettext('German')),
('es', ugettext('Spanish')),
('zh-hans', ugettext('Chinese Simplified')),
('ru', ugettext('Russian')),
('ko', ugettext('Korean')),
)
TEMPLATES = [
@@ -218,6 +220,14 @@ LOGGING = {
'maxBytes': 1024 * 1024 * 5, # edit this line to change max log file size
'backupCount': 5, # edit this line to change number of log backups
},
'extension_file': {
'level': 'DEBUG',
'class': 'logging.handlers.RotatingFileHandler',
'filename': os.path.join(BASE_DIR, 'log/extensions.log'),
'formatter': 'verbose',
'maxBytes': 1024 * 1024 * 5, # edit this line to change max log file size
'backupCount': 5, # edit this line to change number of log backups
},
'console': {
'level': 'DEBUG', # edit this line to change logging level to console
'class': 'logging.StreamHandler',
@@ -234,6 +244,10 @@ LOGGING = {
'handlers': ['log_file', 'console', 'notifications'],
'level': 'DEBUG',
},
'extensions': {
'handlers': ['extension_file', 'console'],
'level': 'DEBUG',
},
'django': {
'handlers': ['log_file', 'console'],
'level': 'ERROR',

View File

@@ -4,3 +4,8 @@ from allianceauth import urls
urlpatterns = [
url(r'', include(urls)),
]
handler500 = 'allianceauth.views.Generic500Redirect'
handler404 = 'allianceauth.views.Generic404Redirect'
handler403 = 'allianceauth.views.Generic403Redirect'
handler400 = 'allianceauth.views.Generic400Redirect'

View File

@@ -17,6 +17,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMix
from django.db import models, IntegrityError
from django.core.exceptions import ObjectDoesNotExist
from django.shortcuts import render, Http404, redirect
from django.utils.translation import gettext_lazy as _
from .forms import ServicePasswordModelForm
@@ -68,7 +69,7 @@ class BaseCreatePasswordServiceAccountView(BaseServiceView, ServiceCredentialsVi
try:
svc_obj = self.model.objects.create(user=request.user)
except IntegrityError:
messages.error(request, "That service account already exists")
messages.error(request, _("That service account already exists"))
return redirect(self.index_redirect)
return render(request, self.template_name,
@@ -100,7 +101,7 @@ class BaseSetPasswordServiceAccountView(ServicesCRUDMixin, BaseServiceView, Upda
def post(self, request, *args, **kwargs):
result = super(BaseSetPasswordServiceAccountView, self).post(request, *args, **kwargs)
if self.get_form().is_valid():
messages.success(request, "Successfully set your {} password".format(self.service_name))
messages.success(request, _("Successfully set your {} password".format(self.service_name)))
return result

View File

@@ -1,4 +1,6 @@
from django.utils.translation import gettext_lazy as _
from allianceauth import hooks
from .hooks import MenuItemHook
from .hooks import ServicesHook
@@ -6,7 +8,7 @@ from .hooks import ServicesHook
class Services(MenuItemHook):
def __init__(self):
MenuItemHook.__init__(self,
'Services',
_('Services'),
'fa fa-cogs fa-fw',
'services:services', 100)

View File

@@ -9,6 +9,29 @@ from allianceauth.hooks import get_hooks
from .models import NameFormatConfig
def get_extension_logger(name):
"""
Takes the name of a plugin/extension and generates a child logger of the extensions logger
to be used by the extension to log events to the extensions logger.
The logging level is decided by whether or not DEBUG is set to true in the project settings. If
DEBUG is set to false, then the logging level is set to INFO.
:param: name: the name of the extension doing the logging
:return: an extensions child logger
"""
import logging
from django.conf import settings
logger = logging.getLogger('extensions.' + name)
logger.name = name
logger.level = logging.INFO
if settings.DEBUG:
logger.level = logging.DEBUG
return logger
class ServicesHook:
"""
Abstract base class for creating a compatible services

View File

@@ -1,38 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2016-09-05 21:40
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0008_alter_user_username_max_length'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='DiscordAuthToken',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('email', models.CharField(max_length=254, unique=True)),
('token', models.CharField(max_length=254)),
('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='GroupCache',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True)),
('groups', models.TextField(default={})),
('service', models.CharField(choices=[(b'discourse', b'discourse'), (b'discord', b'discord')], max_length=254, unique=True)),
],
),
]

View File

@@ -1,22 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2016-10-16 01:35
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('services', '0001_initial'),
]
operations = [
migrations.RemoveField(
model_name='discordauthtoken',
name='user',
),
migrations.DeleteModel(
name='DiscordAuthToken',
),
]

View File

@@ -1,18 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-09-02 06:07
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('services', '0002_auto_20161016_0135'),
]
operations = [
migrations.DeleteModel(
name='GroupCache',
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 2.2.10 on 2020-03-21 13:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('services', '0002_nameformatter'),
]
operations = [
migrations.AlterField(
model_name='nameformatconfig',
name='format',
field=models.CharField(help_text='For information on constructing name formats please see the official documentation, topic "Services Name Formats".', max_length=100),
),
]

View File

@@ -4,14 +4,30 @@ from allianceauth.authentication.models import State
class NameFormatConfig(models.Model):
service_name = models.CharField(max_length=100, blank=False, null=False)
default_to_username = models.BooleanField(default=True, help_text="If a user has no main_character, "
"default to using their Auth username instead.")
format = models.CharField(max_length=100, blank=False, null=False,
help_text='For information on constructing name formats, please see the '
'<a href="https://allianceauth.readthedocs.io/en/latest/features/nameformats">'
'name format documentation</a>')
states = models.ManyToManyField(State, help_text="States to apply this format to. You should only have one "
"formatter for each state for each service.")
service_name = models.CharField(max_length=100, blank=False)
default_to_username = models.BooleanField(
default=True,
help_text=
'If a user has no main_character, '
'default to using their Auth username instead.'
)
format = models.CharField(
max_length=100,
blank=False,
help_text=
'For information on constructing name formats '
'please see the official documentation, '
'topic "Services Name Formats".'
)
states = models.ManyToManyField(
State,
help_text=
"States to apply this format to. You should only have one "
"formatter for each state for each service."
)
def __str__(self):
return '%s: %s' % (
self.service_name, ', '.join([str(x) for x in self.states.all()])
)

View File

@@ -36,7 +36,7 @@ class DiscordService(ServicesHook):
def sync_nickname(self, user):
logger.debug('Syncing %s nickname for user %s' % (self.name, user))
DiscordTasks.update_nickname.delay(user.pk)
DiscordTasks.update_nickname.apply_async(args=[user.pk], countdown=5)
def update_all_groups(self):
logger.debug('Update all %s groups called' % self.name)

View File

@@ -27,7 +27,12 @@ class DiscordViewsTestCase(WebTest):
self.login()
manager.generate_oauth_redirect_url.return_value = '/example.com/oauth/'
response = self.app.get('/discord/activate/', auto_follow=False)
self.assertRedirects(response, expected_url='/example.com/oauth/', target_status_code=404)
self.assertRedirects(
response,
expected_url="/example.com/oauth/",
target_status_code=404,
fetch_redirect_response=False,
)
@mock.patch(MODULE_PATH + '.tasks.DiscordOAuthManager')
def test_callback(self, manager):

View File

@@ -5,8 +5,10 @@ from django.contrib.auth.decorators import login_required
from django.contrib.auth.decorators import permission_required
from django.contrib.auth.decorators import user_passes_test
from django.shortcuts import redirect
from django.utils.translation import gettext_lazy as _
from allianceauth.services.views import superuser_test
from .manager import DiscordOAuthManager
from .tasks import DiscordTasks
@@ -21,10 +23,10 @@ def deactivate_discord(request):
logger.debug("deactivate_discord called by user %s" % request.user)
if DiscordTasks.delete_user(request.user):
logger.info("Successfully deactivated discord for user %s" % request.user)
messages.success(request, 'Deactivated Discord account.')
messages.success(request, _('Deactivated Discord account.'))
else:
logger.error("Unsuccessful attempt to deactivate discord for user %s" % request.user)
messages.error(request, 'An error occurred while processing your Discord account.')
messages.error(request, _('An error occurred while processing your Discord account.'))
return redirect("services:services")
@@ -36,7 +38,7 @@ def reset_discord(request):
logger.info("Successfully deleted discord user for user %s - forwarding to discord activation." % request.user)
return redirect("discord:activate")
logger.error("Unsuccessful attempt to reset discord for user %s" % request.user)
messages.error(request, 'An error occurred while processing your Discord account.')
messages.error(request, _('An error occurred while processing your Discord account.'))
return redirect("services:services")
@@ -57,10 +59,10 @@ def discord_callback(request):
return redirect("services:services")
if DiscordTasks.add_user(request.user, code):
logger.info("Successfully activated Discord for user %s" % request.user)
messages.success(request, 'Activated Discord account.')
messages.success(request, _('Activated Discord account.'))
else:
logger.error("Failed to activate Discord for user %s" % request.user)
messages.error(request, 'An error occurred while processing your Discord account.')
messages.error(request, _('An error occurred while processing your Discord account.'))
return redirect("services:services")

View File

@@ -2,6 +2,7 @@ from django.conf import settings
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.shortcuts import render, redirect
from django.utils.translation import gettext_lazy as _
from .manager import DiscourseManager
from .tasks import DiscourseTasks
@@ -33,12 +34,12 @@ def discourse_sso(request):
# Check if user has access
if not request.user.has_perm(ACCESS_PERM):
messages.error(request, 'You are not authorized to access Discourse.')
messages.error(request, _('You are not authorized to access Discourse.'))
logger.warning('User %s attempted to access Discourse but does not have permission.' % request.user)
return redirect('authentication:dashboard')
if not request.user.profile.main_character:
messages.error(request, "You must have a main character set to access Discourse.")
messages.error(request, _("You must have a main character set to access Discourse."))
logger.warning('User %s attempted to access Discourse but does not have a main character.' % request.user)
return redirect('authentication:characters')
@@ -48,7 +49,7 @@ def discourse_sso(request):
signature = request.GET.get('sig')
if None in [payload, signature]:
messages.error(request, 'No SSO payload or signature. Please contact support if this problem persists.')
messages.error(request, _('No SSO payload or signature. Please contact support if this problem persists.'))
return redirect('authentication:dashboard')
# Validate the payload
@@ -58,7 +59,7 @@ def discourse_sso(request):
assert 'nonce' in decoded
assert len(payload) > 0
except AssertionError:
messages.error(request, 'Invalid payload. Please contact support if this problem persists.')
messages.error(request, _('Invalid payload. Please contact support if this problem persists.'))
return redirect('authentication:dashboard')
key = str(settings.DISCOURSE_SSO_SECRET).encode('utf-8')
@@ -66,7 +67,7 @@ def discourse_sso(request):
this_signature = h.hexdigest()
if this_signature != signature:
messages.error(request, 'Invalid payload. Please contact support if this problem persists.')
messages.error(request, _('Invalid payload. Please contact support if this problem persists.'))
return redirect('authentication:dashboard')
## Build the return payload

View File

@@ -3,6 +3,7 @@ import logging
from django.contrib import messages
from django.contrib.auth.decorators import login_required, permission_required
from django.shortcuts import render, redirect
from django.utils.translation import gettext_lazy as _
from allianceauth.services.forms import ServicePasswordForm
from .manager import Ips4Manager
@@ -27,7 +28,7 @@ def activate_ips4(request):
logger.debug("Updated authserviceinfo for user %s with IPS4 credentials." % request.user)
# update_ips4_groups.delay(request.user.pk)
logger.info("Successfully activated IPS4 for user %s" % request.user)
messages.success(request, 'Activated IPSuite4 account.')
messages.success(request, _('Activated IPSuite4 account.'))
credentials = {
'username': result[0],
'password': result[1],
@@ -36,7 +37,7 @@ def activate_ips4(request):
context={'credentials': credentials, 'service': 'IPSuite4'})
else:
logger.error("Unsuccessful attempt to activate IPS4 for user %s" % request.user)
messages.error(request, 'An error occurred while processing your IPSuite4 account.')
messages.error(request, _('An error occurred while processing your IPSuite4 account.'))
return redirect("services:services")
@@ -49,7 +50,7 @@ def reset_ips4_password(request):
# false we failed
if result != "":
logger.info("Successfully reset IPS4 password for user %s" % request.user)
messages.success(request, 'Reset IPSuite4 password.')
messages.success(request, _('Reset IPSuite4 password.'))
credentials = {
'username': request.user.ips4.username,
'password': result,
@@ -58,7 +59,7 @@ def reset_ips4_password(request):
context={'credentials': credentials, 'service': 'IPSuite4'})
logger.error("Unsuccessful attempt to reset IPS4 password for user %s" % request.user)
messages.error(request, 'An error occurred while processing your IPSuite4 account.')
messages.error(request, _('An error occurred while processing your IPSuite4 account.'))
return redirect("services:services")
@@ -76,10 +77,10 @@ def set_ips4_password(request):
result = Ips4Manager.update_custom_password(request.user.ips4.username, plain_password=password)
if result != "":
logger.info("Successfully set IPS4 password for user %s" % request.user)
messages.success(request, 'Set IPSuite4 password.')
messages.success(request, _('Set IPSuite4 password.'))
else:
logger.error("Failed to install custom IPS4 password for user %s" % request.user)
messages.error(request, 'An error occurred while processing your IPSuite4 account.')
messages.error(request, _('An error occurred while processing your IPSuite4 account.'))
return redirect('services:services')
else:
logger.debug("Request is not type POST - providing empty form.")
@@ -96,9 +97,9 @@ def deactivate_ips4(request):
logger.debug("deactivate_ips4 called by user %s" % request.user)
if Ips4Tasks.delete_user(request.user):
logger.info("Successfully deactivated IPS4 for user %s" % request.user)
messages.success(request, 'Deactivated IPSuite4 account.')
messages.success(request, _('Deactivated IPSuite4 account.'))
else:
logger.error("Unsuccessful attempt to deactivate IPS4 for user %s" % request.user)
messages.error(request, 'An error occurred while processing your IPSuite4 account.')
messages.error(request, _('An error occurred while processing your IPSuite4 account.'))
return redirect("services:services")

View File

@@ -40,6 +40,11 @@ class MumbleService(ServicesHook):
if MumbleTasks.has_account(user):
MumbleTasks.update_groups.delay(user.pk)
def sync_nickname(self, user):
logger.debug("Updating %s nickname for %s" % (self.name, user))
if MumbleTasks.has_account(user):
MumbleTasks.update_display_name.apply_async(args=[user.pk], countdown=5) # cooldown on this task to ensure DB clean when syncing
def validate_user(self, user):
if MumbleTasks.has_account(user) and not self.service_active_for_user(user):
self.delete_user(user, notify_user=True)

View File

@@ -0,0 +1,17 @@
# Generated by Django 2.2.9 on 2020-03-16 07:49
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mumble', '0007_not_null_user'),
]
operations = [
migrations.AddField(
model_name='mumbleuser',
name='display_name',
field=models.CharField(max_length=254, null=True),
)
]

View File

@@ -0,0 +1,37 @@
from django.db import migrations, models
from ..auth_hooks import MumbleService
from allianceauth.services.hooks import NameFormatter
def fwd_func(apps, schema_editor):
MumbleUser = apps.get_model("mumble", "MumbleUser")
db_alias = schema_editor.connection.alias
all_users = MumbleUser.objects.using(db_alias).all()
for user in all_users:
display_name = NameFormatter(MumbleService(), user.user).format_name()
user.display_name = display_name
user.save()
def rev_func(apps, schema_editor):
MumbleUser = apps.get_model("mumble", "MumbleUser")
db_alias = schema_editor.connection.alias
all_users = MumbleUser.objects.using(db_alias).all()
for user in all_users:
user.display_name = None
user.save()
class Migration(migrations.Migration):
dependencies = [
('mumble', '0008_mumbleuser_display_name'),
]
operations = [
migrations.RunPython(fwd_func, rev_func),
migrations.AlterField(
model_name='mumbleuser',
name='display_name',
field=models.CharField(max_length=254, unique=True),
preserve_default=False,
),
]

View File

@@ -15,10 +15,14 @@ class MumbleManager(models.Manager):
HASH_FN = 'bcrypt-sha256'
@staticmethod
def get_username(user):
def get_display_name(user):
from .auth_hooks import MumbleService
return NameFormatter(MumbleService(), user).format_name()
@staticmethod
def get_username(user):
return user.profile.main_character.character_name # main character as the user.username may be incorect
@staticmethod
def sanitise_username(username):
return username.replace(" ", "_")
@@ -32,20 +36,26 @@ class MumbleManager(models.Manager):
return bcrypt_sha256.encrypt(password.encode('utf-8'))
def create(self, user):
username = self.get_username(user)
logger.debug("Creating mumble user with username {}".format(username))
username_clean = self.sanitise_username(username)
password = self.generate_random_pass()
pwhash = self.gen_pwhash(password)
logger.debug("Proceeding with mumble user creation: clean username {}, pwhash starts with {}".format(
username_clean, pwhash[0:5]))
logger.info("Creating mumble user {}".format(username_clean))
try:
username = self.get_username(user)
logger.debug("Creating mumble user with username {}".format(username))
username_clean = self.sanitise_username(username)
display_name = self.get_display_name(user)
password = self.generate_random_pass()
pwhash = self.gen_pwhash(password)
logger.debug("Proceeding with mumble user creation: clean username {}, pwhash starts with {}".format(
username_clean, pwhash[0:5]))
logger.info("Creating mumble user {}".format(username_clean))
result = super(MumbleManager, self).create(user=user, username=username_clean,
pwhash=pwhash, hashfn=self.HASH_FN)
result.update_groups()
result.credentials.update({'username': result.username, 'password': password})
return result
result = super(MumbleManager, self).create(user=user, username=username_clean,
pwhash=pwhash, hashfn=self.HASH_FN,
display_name=display_name)
result.update_groups()
result.credentials.update({'username': result.username, 'password': password})
return result
except AttributeError: # No Main or similar errors
return False
return False
def user_exists(self, username):
return self.filter(username=username).exists()
@@ -59,6 +69,8 @@ class MumbleUser(AbstractServiceModel):
objects = MumbleManager()
display_name = models.CharField(max_length=254, unique=True)
def __str__(self):
return self.username
@@ -91,6 +103,12 @@ class MumbleUser(AbstractServiceModel):
self.save()
return True
def update_display_name(self):
logger.info("Updating mumble user {} display name".format(self.user))
self.display_name = MumbleManager.get_display_name(self.user)
self.save()
return True
class Meta:
permissions = (
("access_mumble", u"Can access the Mumble service"),

View File

@@ -45,9 +45,37 @@ class MumbleTasks:
logger.debug("User %s does not have a mumble account, skipping" % user)
return False
@staticmethod
@shared_task(bind=True, name="mumble.update_display_name", base=QueueOnce)
def update_display_name(self, pk):
user = User.objects.get(pk=pk)
logger.debug("Updating mumble groups for user %s" % user)
if MumbleTasks.has_account(user):
try:
if not user.mumble.update_display_name():
raise Exception("Display Name Sync failed")
logger.debug("Updated user %s mumble display name." % user)
return True
except MumbleUser.DoesNotExist:
logger.info("Mumble display name sync failed for {}, user does not have a mumble account".format(user))
except:
logger.exception("Mumble display name sync failed for %s, retrying in 10 mins" % user)
raise self.retry(countdown=60 * 10)
else:
logger.debug("User %s does not have a mumble account, skipping" % user)
return False
@staticmethod
@shared_task(name="mumble.update_all_groups")
def update_all_groups():
logger.debug("Updating ALL mumble groups")
for mumble_user in MumbleUser.objects.exclude(username__exact=''):
MumbleTasks.update_groups.delay(mumble_user.user.pk)
@staticmethod
@shared_task(name="mumble.update_all_display_names")
def update_all_display_names():
logger.debug("Updating ALL mumble display names")
for mumble_user in MumbleUser.objects.exclude(username__exact=''):
MumbleTasks.update_display_name.delay(mumble_user.user.pk)

View File

@@ -25,6 +25,9 @@ class MumbleHooksTestCase(TestCase):
def setUp(self):
self.member = 'member_user'
member = AuthUtils.create_member(self.member)
AuthUtils.add_main_character(member, 'auth_member', '12345', corp_id='111', corp_name='Test Corporation',
corp_ticker='TESTR')
member = User.objects.get(pk=member.pk)
MumbleUser.objects.create(user=member)
self.none_user = 'none_user'
none_user = AuthUtils.create_user(self.none_user)
@@ -122,23 +125,45 @@ class MumbleViewsTestCase(TestCase):
self.member.save()
AuthUtils.add_main_character(self.member, 'auth_member', '12345', corp_id='111', corp_name='Test Corporation',
corp_ticker='TESTR')
self.member = User.objects.get(pk=self.member.pk)
add_permissions()
def login(self):
self.client.force_login(self.member)
def test_activate(self):
def test_activate_update(self):
self.login()
expected_username = '[TESTR]auth_member'
expected_username = 'auth_member'
expected_displayname = '[TESTR]auth_member'
response = self.client.get(urls.reverse('mumble:activate'), follow=False)
self.assertEqual(response.status_code, 200)
self.assertContains(response, expected_username)
# create
mumble_user = MumbleUser.objects.get(user=self.member)
self.assertEqual(mumble_user.username, expected_username)
self.assertTrue(MumbleUser.objects.user_exists(expected_username))
self.assertEqual(str(mumble_user), expected_username)
self.assertEqual(mumble_user.display_name, expected_displayname)
self.assertTrue(mumble_user.pwhash)
self.assertIn('Guest', mumble_user.groups)
self.assertIn('Member', mumble_user.groups)
self.assertIn(',', mumble_user.groups)
# test update
self.member.profile.main_character.character_name = "auth_member_updated"
self.member.profile.main_character.corporation_ticker = "TESTU"
self.member.profile.main_character.save()
mumble_user.update_display_name()
mumble_user = MumbleUser.objects.get(user=self.member)
expected_displayname = '[TESTU]auth_member_updated'
self.assertEqual(mumble_user.username, expected_username)
self.assertTrue(MumbleUser.objects.user_exists(expected_username))
self.assertEqual(str(mumble_user), expected_username)
self.assertEqual(mumble_user.display_name, expected_displayname)
self.assertTrue(mumble_user.pwhash)
self.assertIn('Guest', mumble_user.groups)
self.assertIn('Member', mumble_user.groups)
self.assertIn(',', mumble_user.groups)
def test_deactivate_post(self):
self.login()
@@ -171,7 +196,6 @@ class MumbleViewsTestCase(TestCase):
self.assertTemplateUsed(response, 'services/service_credentials.html')
self.assertContains(response, 'auth_member')
class MumbleManagerTestCase(TestCase):
def setUp(self):
from .models import MumbleManager

View File

@@ -2,6 +2,7 @@ import logging
from django.conf import settings
from django.template.loader import render_to_string
from django.utils.translation import gettext_lazy as _
from allianceauth import hooks
from allianceauth.services.hooks import ServicesHook, MenuItemHook
@@ -22,7 +23,7 @@ class OpenfireService(ServicesHook):
@property
def title(self):
return "Jabber"
return _("Jabber")
def delete_user(self, user, notify_user=False):
logger.debug('Deleting user %s %s account' % (user, self.name))
@@ -74,7 +75,7 @@ def register_service():
class JabberBroadcast(MenuItemHook):
def __init__(self):
MenuItemHook.__init__(self,
'Jabber Broadcast',
_('Jabber Broadcast'),
'fa fa-lock fa-fw fa-bullhorn',
'openfire:broadcast')
@@ -87,7 +88,7 @@ class JabberBroadcast(MenuItemHook):
class FleetBroadcastFormatter(MenuItemHook):
def __init__(self):
MenuItemHook.__init__(self,
'Fleet Broadcast Formatter',
_('Fleet Broadcast Formatter'),
'fa fa-lock fa-fw fa-space-shuttle',
'services:fleet_format_tool')

View File

@@ -5,8 +5,10 @@ from django.contrib import messages
from django.contrib.auth.decorators import login_required, permission_required
from django.contrib.auth.models import Group
from django.shortcuts import render, redirect
from django.utils.translation import gettext_lazy as _
from allianceauth.services.forms import ServicePasswordForm
from .forms import JabberBroadcastForm
from .manager import OpenfireManager, PingBotException
from .models import OpenfireUser
@@ -30,7 +32,7 @@ def activate_jabber(request):
logger.debug("Updated authserviceinfo for user %s with jabber credentials. Updating groups." % request.user)
OpenfireTasks.update_groups.delay(request.user.pk)
logger.info("Successfully activated jabber for user %s" % request.user)
messages.success(request, 'Activated jabber account.')
messages.success(request, _('Activated jabber account.'))
credentials = {
'username': info[0],
'password': info[1],
@@ -39,7 +41,7 @@ def activate_jabber(request):
context={'credentials': credentials, 'service': 'Jabber'})
else:
logger.error("Unsuccessful attempt to activate jabber for user %s" % request.user)
messages.error(request, 'An error occurred while processing your jabber account.')
messages.error(request, _('An error occurred while processing your jabber account.'))
return redirect("services:services")
@@ -52,7 +54,7 @@ def deactivate_jabber(request):
messages.success(request, 'Deactivated jabber account.')
else:
logger.error("Unsuccessful attempt to deactivate jabber for user %s" % request.user)
messages.error(request, 'An error occurred while processing your jabber account.')
messages.error(request, _('An error occurred while processing your jabber account.'))
return redirect("services:services")
@@ -65,7 +67,7 @@ def reset_jabber_password(request):
# If our username is blank means we failed
if result != "":
logger.info("Successfully reset jabber password for user %s" % request.user)
messages.success(request, 'Reset jabber password.')
messages.success(request, _('Reset jabber password.'))
credentials = {
'username': request.user.openfire.username,
'password': result,
@@ -73,7 +75,7 @@ def reset_jabber_password(request):
return render(request, 'services/service_credentials.html',
context={'credentials': credentials, 'service': 'Jabber'})
logger.error("Unsuccessful attempt to reset jabber for user %s" % request.user)
messages.error(request, 'An error occurred while processing your jabber account.')
messages.error(request, _('An error occurred while processing your jabber account.'))
return redirect("services:services")
@@ -114,7 +116,7 @@ def jabber_broadcast_view(request):
OpenfireManager.send_broadcast_message(group_to_send, message_to_send)
messages.success(request, 'Sent jabber broadcast to %s' % group_to_send)
messages.success(request, _('Sent jabber broadcast to %s' % group_to_send))
logger.info("Sent jabber broadcast on behalf of user %s" % request.user)
except PingBotException as e:
messages.error(request, e)
@@ -143,10 +145,10 @@ def set_jabber_password(request):
result = OpenfireManager.update_user_pass(request.user.openfire.username, password=password)
if result != "":
logger.info("Successfully set jabber password for user %s" % request.user)
messages.success(request, 'Set jabber password.')
messages.success(request, _('Set jabber password.'))
else:
logger.error("Failed to install custom jabber password for user %s" % request.user)
messages.error(request, 'An error occurred while processing your jabber account.')
messages.error(request, _('An error occurred while processing your jabber account.'))
return redirect("services:services")
else:
logger.debug("Request is not type POST - providing empty form.")

View File

@@ -3,8 +3,10 @@ import logging
from django.contrib import messages
from django.contrib.auth.decorators import login_required, permission_required
from django.shortcuts import render, redirect
from django.utils.translation import gettext_lazy as _
from allianceauth.services.forms import ServicePasswordForm
from .manager import Phpbb3Manager
from .models import Phpbb3User
from .tasks import Phpbb3Tasks
@@ -29,7 +31,7 @@ def activate_forum(request):
logger.debug("Updated authserviceinfo for user %s with forum credentials. Updating groups." % request.user)
Phpbb3Tasks.update_groups.delay(request.user.pk)
logger.info("Successfully activated forum for user %s" % request.user)
messages.success(request, 'Activated forum account.')
messages.success(request, _('Activated forum account.'))
credentials = {
'username': result[0],
'password': result[1],
@@ -38,7 +40,7 @@ def activate_forum(request):
context={'credentials': credentials, 'service': 'Forum'})
else:
logger.error("Unsuccessful attempt to activate forum for user %s" % request.user)
messages.error(request, 'An error occurred while processing your forum account.')
messages.error(request, _('An error occurred while processing your forum account.'))
return redirect("services:services")
@@ -49,10 +51,10 @@ def deactivate_forum(request):
# false we failed
if Phpbb3Tasks.delete_user(request.user):
logger.info("Successfully deactivated forum for user %s" % request.user)
messages.success(request, 'Deactivated forum account.')
messages.success(request, _('Deactivated forum account.'))
else:
logger.error("Unsuccessful attempt to activate forum for user %s" % request.user)
messages.error(request, 'An error occurred while processing your forum account.')
messages.error(request, _('An error occurred while processing your forum account.'))
return redirect("services:services")
@@ -66,7 +68,7 @@ def reset_forum_password(request):
# false we failed
if result != "":
logger.info("Successfully reset forum password for user %s" % request.user)
messages.success(request, 'Reset forum password.')
messages.success(request, _('Reset forum password.'))
credentials = {
'username': request.user.phpbb3.username,
'password': result,
@@ -75,7 +77,7 @@ def reset_forum_password(request):
context={'credentials': credentials, 'service': 'Forum'})
logger.error("Unsuccessful attempt to reset forum password for user %s" % request.user)
messages.error(request, 'An error occurred while processing your forum account.')
messages.error(request, _('An error occurred while processing your forum account.'))
return redirect("services:services")
@@ -95,10 +97,10 @@ def set_forum_password(request):
password=password)
if result != "":
logger.info("Successfully set forum password for user %s" % request.user)
messages.success(request, 'Set forum password.')
messages.success(request, _('Set forum password.'))
else:
logger.error("Failed to install custom forum password for user %s" % request.user)
messages.error(request, 'An error occurred while processing your forum account.')
messages.error(request, _('An error occurred while processing your forum account.'))
return redirect("services:services")
else:
logger.debug("Request is not type POST - providing empty form.")

View File

@@ -3,8 +3,10 @@ import logging
from django.contrib import messages
from django.contrib.auth.decorators import login_required, permission_required
from django.shortcuts import render, redirect
from django.utils.translation import gettext_lazy as _
from allianceauth.services.forms import ServicePasswordForm
from .manager import SmfManager
from .models import SmfUser
from .tasks import SmfTasks
@@ -29,7 +31,7 @@ def activate_smf(request):
logger.debug("Updated authserviceinfo for user %s with smf credentials. Updating groups." % request.user)
SmfTasks.update_groups.delay(request.user.pk)
logger.info("Successfully activated smf for user %s" % request.user)
messages.success(request, 'Activated SMF account.')
messages.success(request, _('Activated SMF account.'))
credentials = {
'username': result[0],
'password': result[1],
@@ -38,7 +40,7 @@ def activate_smf(request):
context={'credentials': credentials, 'service': 'SMF'})
else:
logger.error("Unsuccessful attempt to activate smf for user %s" % request.user)
messages.error(request, 'An error occurred while processing your SMF account.')
messages.error(request, _('An error occurred while processing your SMF account.'))
return redirect("services:services")
@@ -50,10 +52,10 @@ def deactivate_smf(request):
# false we failed
if result:
logger.info("Successfully deactivated smf for user %s" % request.user)
messages.success(request, 'Deactivated SMF account.')
messages.success(request, _('Deactivated SMF account.'))
else:
logger.error("Unsuccessful attempt to activate smf for user %s" % request.user)
messages.error(request, 'An error occurred while processing your SMF account.')
messages.error(request, _('An error occurred while processing your SMF account.'))
return redirect("services:services")
@@ -67,7 +69,7 @@ def reset_smf_password(request):
# false we failed
if result != "":
logger.info("Successfully reset smf password for user %s" % request.user)
messages.success(request, 'Reset SMF password.')
messages.success(request, _('Reset SMF password.'))
credentials = {
'username': request.user.smf.username,
'password': result,
@@ -75,7 +77,7 @@ def reset_smf_password(request):
return render(request, 'services/service_credentials.html',
context={'credentials': credentials, 'service': 'SMF'})
logger.error("Unsuccessful attempt to reset smf password for user %s" % request.user)
messages.error(request, 'An error occurred while processing your SMF account.')
messages.error(request, _('An error occurred while processing your SMF account.'))
return redirect("services:services")
@@ -95,10 +97,10 @@ def set_smf_password(request):
password=password)
if result != "":
logger.info("Successfully set smf password for user %s" % request.user)
messages.success(request, 'Set SMF password.')
messages.success(request, _('Set SMF password.'))
else:
logger.error("Failed to install custom smf password for user %s" % request.user)
messages.error(request, 'An error occurred while processing your SMF account.')
messages.error(request, _('An error occurred while processing your SMF account.'))
return redirect("services:services")
else:
logger.debug("Request is not type POST - providing empty form.")

View File

@@ -4,6 +4,8 @@ from django.contrib import messages
from django.contrib.auth.decorators import login_required, permission_required
from django.shortcuts import render, redirect
from django.conf import settings
from django.utils.translation import gettext_lazy as _
from .manager import Teamspeak3Manager
from .forms import TeamspeakJoinForm
from .models import Teamspeak3User
@@ -29,10 +31,10 @@ def activate_teamspeak3(request):
Teamspeak3User.objects.update_or_create(user=request.user, defaults={'uid': result[0], 'perm_key': result[1]})
logger.debug("Updated authserviceinfo for user %s with TS3 credentials. Updating groups." % request.user)
logger.info("Successfully activated TS3 for user %s" % request.user)
messages.success(request, 'Activated TeamSpeak3 account.')
messages.success(request, _('Activated TeamSpeak3 account.'))
return redirect("teamspeak3:verify")
logger.error("Unsuccessful attempt to activate TS3 for user %s" % request.user)
messages.error(request, 'An error occurred while processing your TeamSpeak3 account.')
messages.error(request, _('An error occurred while processing your TeamSpeak3 account.'))
return redirect("services:services")
@@ -66,10 +68,10 @@ def deactivate_teamspeak3(request):
logger.debug("deactivate_teamspeak3 called by user %s" % request.user)
if Teamspeak3Tasks.has_account(request.user) and Teamspeak3Tasks.delete_user(request.user):
logger.info("Successfully deactivated TS3 for user %s" % request.user)
messages.success(request, 'Deactivated TeamSpeak3 account.')
messages.success(request, _('Deactivated TeamSpeak3 account.'))
else:
logger.error("Unsuccessful attempt to deactivate TS3 for user %s" % request.user)
messages.error(request, 'An error occurred while processing your TeamSpeak3 account.')
messages.error(request, _('An error occurred while processing your TeamSpeak3 account.'))
return redirect("services:services")
@@ -92,8 +94,8 @@ def reset_teamspeak3_perm(request):
logger.debug("Updated authserviceinfo for user %s with TS3 credentials. Updating groups." % request.user)
Teamspeak3Tasks.update_groups.delay(request.user.pk)
logger.info("Successfully reset TS3 permission key for user %s" % request.user)
messages.success(request, 'Reset TeamSpeak3 permission key.')
messages.success(request, _('Reset TeamSpeak3 permission key.'))
else:
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.')
messages.error(request, _('An error occurred while processing your TeamSpeak3 account.'))
return redirect("services:services")

View File

@@ -3,8 +3,10 @@ import logging
from django.contrib import messages
from django.contrib.auth.decorators import login_required, permission_required
from django.shortcuts import render, redirect
from django.utils.translation import gettext_lazy as _
from allianceauth.services.forms import ServicePasswordForm
from .manager import XenForoManager
from .models import XenforoUser
from .tasks import XenforoTasks
@@ -25,7 +27,7 @@ def activate_xenforo_forum(request):
if result['response']['status_code'] == 200:
XenforoUser.objects.update_or_create(user=request.user, defaults={'username': result['username']})
logger.info("Updated user %s with XenForo credentials. Updating groups." % request.user)
messages.success(request, 'Activated XenForo account.')
messages.success(request, _('Activated XenForo account.'))
credentials = {
'username': result['username'],
'password': result['password'],
@@ -35,7 +37,7 @@ def activate_xenforo_forum(request):
else:
logger.error("Unsuccessful attempt to activate xenforo for user %s" % request.user)
messages.error(request, 'An error occurred while processing your XenForo account.')
messages.error(request, _('An error occurred while processing your XenForo account.'))
return redirect("services:services")
@@ -45,9 +47,9 @@ def deactivate_xenforo_forum(request):
logger.debug("deactivate_xenforo_forum called by user %s" % request.user)
if XenforoTasks.delete_user(request.user):
logger.info("Successfully deactivated XenForo for user %s" % request.user)
messages.success(request, 'Deactivated XenForo account.')
messages.success(request, _('Deactivated XenForo account.'))
else:
messages.error(request, 'An error occurred while processing your XenForo account.')
messages.error(request, _('An error occurred while processing your XenForo account.'))
return redirect("services:services")
@@ -60,7 +62,7 @@ def reset_xenforo_password(request):
# Based on XenAPI's response codes
if result['response']['status_code'] == 200:
logger.info("Successfully reset XenForo password for user %s" % request.user)
messages.success(request, 'Reset XenForo account password.')
messages.success(request, _('Reset XenForo account password.'))
credentials = {
'username': request.user.xenforo.username,
'password': result['password'],
@@ -68,7 +70,7 @@ def reset_xenforo_password(request):
return render(request, 'services/service_credentials.html',
context={'credentials': credentials, 'service': 'XenForo'})
logger.error("Unsuccessful attempt to reset XenForo password for user %s" % request.user)
messages.error(request, 'An error occurred while processing your XenForo account.')
messages.error(request, _('An error occurred while processing your XenForo account.'))
return redirect("services:services")
@@ -86,10 +88,10 @@ def set_xenforo_password(request):
result = XenForoManager.update_user_password(request.user.xenforo.username, password)
if result['response']['status_code'] == 200:
logger.info("Successfully reset XenForo password for user %s" % request.user)
messages.success(request, 'Changed XenForo password.')
messages.success(request, _('Changed XenForo password.'))
else:
logger.error("Failed to install custom XenForo password for user %s" % request.user)
messages.error(request, 'An error occurred while processing your XenForo account.')
messages.error(request, _('An error occurred while processing your XenForo account.'))
return redirect('services:services')
else:
logger.debug("Request is not type POST - providing empty form.")

View File

@@ -1,6 +1,7 @@
import logging
from django.contrib.auth.models import User, Group, Permission
from django.core.exceptions import ObjectDoesNotExist
from django.db import transaction
from django.db.models.signals import m2m_changed
from django.db.models.signals import pre_delete
@@ -11,6 +12,7 @@ from .tasks import disable_user
from allianceauth.authentication.models import State, UserProfile
from allianceauth.authentication.signals import state_changed
from allianceauth.eveonline.models import EveCharacter
logger = logging.getLogger(__name__)
@@ -157,14 +159,45 @@ def disable_services_on_inactive(sender, instance, *args, **kwargs):
@receiver(pre_save, sender=UserProfile)
def disable_services_on_no_main(sender, instance, *args, **kwargs):
if not instance.pk:
def process_main_character_change(sender, instance, *args, **kwargs):
if not instance.pk: # ignore
# new model being created
return
try:
old_instance = UserProfile.objects.get(pk=instance.pk)
if old_instance.main_character and not instance.main_character:
if old_instance.main_character and not instance.main_character: # lost main char disable services
logger.info("Disabling services due to loss of main character for user {0}".format(instance.user))
disable_user(instance.user)
elif old_instance.main_character is not instance.main_character: # swapping/changing main character
logger.info("Updating Names due to change of main character for user {0}".format(instance.user))
for svc in ServicesHook.get_services():
try:
svc.validate_user(instance.user)
svc.sync_nickname(instance.user)
except:
logger.exception('Exception running sync_nickname for services module %s on user %s' % (svc, instance))
except UserProfile.DoesNotExist:
pass
@receiver(pre_save, sender=EveCharacter)
def process_main_character_update(sender, instance, *args, **kwargs):
try:
if instance.userprofile:
old_instance = EveCharacter.objects.get(pk=instance.pk)
if not instance.character_name == old_instance.character_name or \
not instance.corporation_name == old_instance.corporation_name or \
not instance.alliance_name == old_instance.alliance_name:
logger.info("syncing service nickname for user {0}".format(instance.userprofile.user))
for svc in ServicesHook.get_services():
try:
svc.validate_user(instance.userprofile.user)
svc.sync_nickname(instance.userprofile.user)
except:
logger.exception('Exception running sync_nickname for services module %s on user %s' % (svc, instance))
except ObjectDoesNotExist: # not a main char ignore
pass

View File

@@ -0,0 +1,17 @@
from django.test import TestCase
from allianceauth.tests.auth_utils import AuthUtils
from ..models import NameFormatConfig
class TestNameFormatConfig(TestCase):
def test_str(self):
obj = NameFormatConfig.objects.create(
service_name='mumble',
format='{{character_name}}'
)
obj.states.add(AuthUtils.get_member_state())
obj.states.add(AuthUtils.get_guest_state())
expected = 'mumble: Member, Guest'
self.assertEqual(str(obj), expected)

View File

@@ -1,21 +1,17 @@
from allianceauth import NAME
from esi.clients import esi_client_factory
import requests
import logging
import os
import requests
from allianceauth import NAME
from allianceauth.eveonline.providers import provider
logger = logging.getLogger(__name__)
SWAGGER_SPEC_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'swagger.json')
"""
Swagger Operations:
get_killmails_killmail_id_killmail_hash
"""
class SRPManager:
def __init__(self):
pass
@staticmethod
def get_kill_id(killboard_link):
num_set = '0123456789'
@@ -34,18 +30,23 @@ class SRPManager:
if result:
killmail_id = result['killmail_id']
killmail_hash = result['zkb']['hash']
c = esi_client_factory(spec_file=SWAGGER_SPEC_PATH)
km = c.Killmails.get_killmails_killmail_id_killmail_hash(killmail_id=killmail_id,
killmail_hash=killmail_hash).result()
c = provider.client
km = c.Killmails.get_killmails_killmail_id_killmail_hash(
killmail_id=killmail_id,
killmail_hash=killmail_hash
).result()
else:
raise ValueError("Invalid Kill ID")
if km:
ship_type = km['victim']['ship_type_id']
logger.debug("Ship type for kill ID %s is %s" % (kill_id, ship_type))
logger.debug(
"Ship type for kill ID %s is %s" % (kill_id, ship_type)
)
ship_value = result['zkb']['totalValue']
logger.debug("Total loss value for kill id %s is %s" % (kill_id, ship_value))
logger.debug(
"Total loss value for kill id %s is %s" % (kill_id, ship_value)
)
victim_id = km['victim']['character_id']
return ship_type, ship_value, victim_id
else:
raise ValueError("Invalid Kill ID or Hash.")

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
# Create your tests here.

View File

View File

@@ -0,0 +1,72 @@
import inspect
import json
import os
from unittest.mock import patch, Mock
from django.test import TestCase
from ..managers import SRPManager
MODULE_PATH = 'allianceauth.srp.managers'
currentdir = os.path.dirname(os.path.abspath(inspect.getfile(
inspect.currentframe()
)))
def load_data(filename):
"""loads given JSON file from `testdata` sub folder and returns content"""
with open(
currentdir + '/testdata/%s.json' % filename, 'r', encoding='utf-8'
) as f:
data = json.load(f)
return data
class TestSrpManager(TestCase):
def test_can_extract_kill_id(self):
link = 'https://zkillboard.com/kill/81973979/'
expected = 81973979
self.assertEqual(int(SRPManager.get_kill_id(link)), expected)
@patch(MODULE_PATH + '.provider')
@patch(MODULE_PATH + '.requests.get')
def test_can_get_kill_data(self, mock_get, mock_provider):
mock_get.return_value.json.return_value = load_data(
'zkillboard_killmail_api_81973979'
)
mock_provider.client.Killmails.\
get_killmails_killmail_id_killmail_hash.return_value.\
result.return_value = load_data(
'get_killmails_killmail_id_killmail_hash_81973979'
)
ship_type, ship_value, victim_id = SRPManager.get_kill_data(81973979)
self.assertEqual(ship_type, 19720)
self.assertEqual(ship_value, 3177859026.86)
self.assertEqual(victim_id, 93330670)
@patch(MODULE_PATH + '.requests.get')
def test_invalid_id_for_zkb_raises_exception(self, mock_get):
mock_get.return_value.json.return_value = ['']
with self.assertRaises(ValueError):
ship_type, ship_value, victim_id = SRPManager.get_kill_data(81973979)
@patch(MODULE_PATH + '.provider')
@patch(MODULE_PATH + '.requests.get')
def test_invalid_id_for_esi_raises_exception(
self, mock_get, mock_provider
):
mock_get.return_value.json.return_value = load_data(
'zkillboard_killmail_api_81973979'
)
mock_provider.client.Killmails.\
get_killmails_killmail_id_killmail_hash.return_value.\
result.return_value = None
with self.assertRaises(ValueError):
ship_type, ship_value, victim_id = SRPManager.get_kill_data(81973979)

View File

@@ -0,0 +1,953 @@
{
"attackers": [
{
"alliance_id": 99009221,
"character_id": 92606407,
"corporation_id": 98343297,
"damage_done": 65236,
"final_blow": false,
"security_status": -6.4,
"ship_type_id": 47271,
"weapon_type_id": 47271
},
{
"alliance_id": 99006941,
"character_id": 95104060,
"corporation_id": 98416134,
"damage_done": 56425,
"final_blow": true,
"security_status": -1.1,
"ship_type_id": 17738,
"weapon_type_id": 2929
},
{
"alliance_id": 1220922756,
"character_id": 92793488,
"corporation_id": 679468421,
"damage_done": 55225,
"final_blow": false,
"security_status": -3.4,
"ship_type_id": 47271,
"weapon_type_id": 47271
},
{
"alliance_id": 99006941,
"character_id": 90376343,
"corporation_id": 98416134,
"damage_done": 51941,
"final_blow": false,
"security_status": 0.6,
"ship_type_id": 17738,
"weapon_type_id": 28215
},
{
"alliance_id": 99006941,
"character_id": 676848606,
"corporation_id": 98416134,
"damage_done": 45906,
"final_blow": false,
"security_status": -1.6,
"ship_type_id": 17738,
"weapon_type_id": 31894
},
{
"alliance_id": 99006941,
"character_id": 96692394,
"corporation_id": 98416134,
"damage_done": 44900,
"final_blow": false,
"security_status": -1.9,
"ship_type_id": 17738,
"weapon_type_id": 31894
},
{
"alliance_id": 99006941,
"character_id": 96624133,
"corporation_id": 98416134,
"damage_done": 44146,
"final_blow": false,
"security_status": -9.1,
"ship_type_id": 17738,
"weapon_type_id": 2929
},
{
"character_id": 95050100,
"corporation_id": 98497860,
"damage_done": 41517,
"final_blow": false,
"security_status": -3,
"ship_type_id": 17738,
"weapon_type_id": 17738
},
{
"alliance_id": 99006941,
"character_id": 458944878,
"corporation_id": 98416134,
"damage_done": 39888,
"final_blow": false,
"security_status": -6.5,
"ship_type_id": 17738,
"weapon_type_id": 28215
},
{
"alliance_id": 99006941,
"character_id": 96029663,
"corporation_id": 98433294,
"damage_done": 39406,
"final_blow": false,
"security_status": -6.7,
"ship_type_id": 17738,
"weapon_type_id": 28215
},
{
"alliance_id": 99006941,
"character_id": 90626300,
"corporation_id": 98416134,
"damage_done": 37808,
"final_blow": false,
"security_status": -0.6,
"ship_type_id": 17738,
"weapon_type_id": 31894
},
{
"alliance_id": 99006941,
"character_id": 90740848,
"corporation_id": 98416134,
"damage_done": 36342,
"final_blow": false,
"security_status": -1.8,
"ship_type_id": 17738,
"weapon_type_id": 2929
},
{
"alliance_id": 99006941,
"character_id": 1105550086,
"corporation_id": 98416134,
"damage_done": 35971,
"final_blow": false,
"security_status": -2.6,
"ship_type_id": 17738,
"weapon_type_id": 28215
},
{
"alliance_id": 99003581,
"character_id": 94727582,
"corporation_id": 98514029,
"damage_done": 33501,
"final_blow": false,
"security_status": 3.2,
"ship_type_id": 17738,
"weapon_type_id": 31894
},
{
"alliance_id": 99006941,
"character_id": 90368224,
"corporation_id": 98416134,
"damage_done": 32116,
"final_blow": false,
"security_status": -2.4,
"ship_type_id": 17738,
"weapon_type_id": 2929
},
{
"alliance_id": 99006941,
"character_id": 90001595,
"corporation_id": 98416134,
"damage_done": 31387,
"final_blow": false,
"security_status": -0.8,
"ship_type_id": 17738,
"weapon_type_id": 2456
},
{
"alliance_id": 99006941,
"character_id": 95278082,
"corporation_id": 98418839,
"damage_done": 31250,
"final_blow": false,
"security_status": 1.8,
"ship_type_id": 17738,
"weapon_type_id": 28215
},
{
"alliance_id": 99006941,
"character_id": 91971344,
"corporation_id": 98217414,
"damage_done": 31247,
"final_blow": false,
"security_status": -1,
"ship_type_id": 29986,
"weapon_type_id": 29986
},
{
"alliance_id": 99006941,
"character_id": 2113448089,
"corporation_id": 98418839,
"damage_done": 30174,
"final_blow": false,
"security_status": -0.4,
"ship_type_id": 17738,
"weapon_type_id": 17738
},
{
"alliance_id": 99006941,
"character_id": 2115912819,
"corporation_id": 98416134,
"damage_done": 29242,
"final_blow": false,
"security_status": -2.1,
"ship_type_id": 17738,
"weapon_type_id": 17738
},
{
"alliance_id": 99006941,
"character_id": 2115885290,
"corporation_id": 98416134,
"damage_done": 28009,
"final_blow": false,
"security_status": -2,
"ship_type_id": 17738,
"weapon_type_id": 17738
},
{
"alliance_id": 99006941,
"character_id": 95746094,
"corporation_id": 98416134,
"damage_done": 27565,
"final_blow": false,
"security_status": -7.8,
"ship_type_id": 17738,
"weapon_type_id": 17738
},
{
"alliance_id": 99003581,
"character_id": 90345487,
"corporation_id": 98514029,
"damage_done": 26016,
"final_blow": false,
"security_status": 0.5,
"ship_type_id": 17738,
"weapon_type_id": 17738
},
{
"alliance_id": 99006941,
"character_id": 2115874625,
"corporation_id": 98416134,
"damage_done": 25679,
"final_blow": false,
"security_status": -1.9,
"ship_type_id": 17738,
"weapon_type_id": 17738
},
{
"alliance_id": 99006941,
"character_id": 2115880975,
"corporation_id": 98416134,
"damage_done": 23320,
"final_blow": false,
"security_status": -3.5,
"ship_type_id": 17738,
"weapon_type_id": 17738
},
{
"alliance_id": 99006941,
"character_id": 96667534,
"corporation_id": 98416134,
"damage_done": 21699,
"final_blow": false,
"security_status": -0.6,
"ship_type_id": 17738,
"weapon_type_id": 17738
},
{
"alliance_id": 99006941,
"character_id": 2115866658,
"corporation_id": 98416134,
"damage_done": 20506,
"final_blow": false,
"security_status": -1.3,
"ship_type_id": 17738,
"weapon_type_id": 17738
},
{
"alliance_id": 99006941,
"character_id": 97105982,
"corporation_id": 98217414,
"damage_done": 19400,
"final_blow": false,
"security_status": 0,
"ship_type_id": 17738,
"weapon_type_id": 17738
},
{
"alliance_id": 99008704,
"character_id": 96110151,
"corporation_id": 98614116,
"damage_done": 17547,
"final_blow": false,
"security_status": -5.5,
"ship_type_id": 17740,
"weapon_type_id": 17740
},
{
"alliance_id": 99009221,
"character_id": 90526637,
"corporation_id": 98343297,
"damage_done": 16791,
"final_blow": false,
"security_status": -1.9,
"ship_type_id": 33157,
"weapon_type_id": 21640
},
{
"alliance_id": 99006941,
"character_id": 2112972140,
"corporation_id": 98416134,
"damage_done": 16749,
"final_blow": false,
"security_status": -1.2,
"ship_type_id": 17738,
"weapon_type_id": 17738
},
{
"alliance_id": 99006941,
"character_id": 2115879470,
"corporation_id": 98416134,
"damage_done": 14402,
"final_blow": false,
"security_status": -3.9,
"ship_type_id": 17738,
"weapon_type_id": 17738
},
{
"alliance_id": 99006941,
"character_id": 95698217,
"corporation_id": 98416134,
"damage_done": 11546,
"final_blow": false,
"security_status": -5.2,
"ship_type_id": 17738,
"weapon_type_id": 17738
},
{
"alliance_id": 99006941,
"character_id": 353190170,
"corporation_id": 98416134,
"damage_done": 10896,
"final_blow": false,
"security_status": -4.7,
"ship_type_id": 17738,
"weapon_type_id": 28215
},
{
"alliance_id": 99006941,
"character_id": 91546798,
"corporation_id": 98217414,
"damage_done": 9872,
"final_blow": false,
"security_status": -0.7,
"ship_type_id": 17738,
"weapon_type_id": 17738
},
{
"alliance_id": 99009221,
"character_id": 91578428,
"corporation_id": 302750157,
"damage_done": 7699,
"final_blow": false,
"security_status": -8.7,
"ship_type_id": 17920,
"weapon_type_id": 2185
},
{
"alliance_id": 99006941,
"character_id": 94105463,
"corporation_id": 98418839,
"damage_done": 5265,
"final_blow": false,
"security_status": -3.3,
"ship_type_id": 49713,
"weapon_type_id": 2488
},
{
"alliance_id": 99006941,
"character_id": 95526304,
"corporation_id": 98418839,
"damage_done": 3967,
"final_blow": false,
"security_status": -8.7,
"ship_type_id": 22474,
"weapon_type_id": 2488
},
{
"alliance_id": 99006941,
"character_id": 90331727,
"corporation_id": 98416134,
"damage_done": 2940,
"final_blow": false,
"security_status": -1.2,
"ship_type_id": 49713,
"weapon_type_id": 2185
},
{
"alliance_id": 99006941,
"character_id": 2115880459,
"corporation_id": 98416134,
"damage_done": 2301,
"final_blow": false,
"security_status": 4.1,
"ship_type_id": 17738,
"weapon_type_id": 17738
},
{
"alliance_id": 99009547,
"character_id": 1832436128,
"corporation_id": 98618666,
"damage_done": 937,
"final_blow": false,
"security_status": -8.1,
"ship_type_id": 17740,
"weapon_type_id": 3186
},
{
"alliance_id": 99009547,
"character_id": 96632877,
"corporation_id": 98618666,
"damage_done": 430,
"final_blow": false,
"security_status": -5.4,
"ship_type_id": 17740,
"weapon_type_id": 3186
},
{
"alliance_id": 99006941,
"character_id": 96146444,
"corporation_id": 98416134,
"damage_done": 126,
"final_blow": false,
"security_status": -1.2,
"ship_type_id": 49713,
"weapon_type_id": 49713
},
{
"character_id": 2116393370,
"corporation_id": 98593091,
"damage_done": 111,
"final_blow": false,
"security_status": 0,
"ship_type_id": 602,
"weapon_type_id": 27321
},
{
"alliance_id": 99006941,
"character_id": 93745147,
"corporation_id": 98418839,
"damage_done": 6,
"final_blow": false,
"security_status": -1.6,
"ship_type_id": 12021,
"weapon_type_id": 2873
},
{
"alliance_id": 99006941,
"character_id": 95610468,
"corporation_id": 98416134,
"damage_done": 4,
"final_blow": false,
"security_status": -2.2,
"ship_type_id": 12017,
"weapon_type_id": 484
},
{
"alliance_id": 99009221,
"character_id": 92304254,
"corporation_id": 98343297,
"damage_done": 1,
"final_blow": false,
"security_status": -9.3,
"ship_type_id": 22474,
"weapon_type_id": 22474
},
{
"alliance_id": 99006941,
"character_id": 96624034,
"corporation_id": 98493618,
"damage_done": 0,
"final_blow": false,
"security_status": -2.1,
"ship_type_id": 12017,
"weapon_type_id": 37611
},
{
"alliance_id": 99006941,
"character_id": 95388762,
"corporation_id": 98416134,
"damage_done": 0,
"final_blow": false,
"security_status": 0.4,
"ship_type_id": 12017,
"weapon_type_id": 3001
},
{
"alliance_id": 99006941,
"character_id": 1290463210,
"corporation_id": 98416134,
"damage_done": 0,
"final_blow": false,
"security_status": -1.9,
"ship_type_id": 49713,
"weapon_type_id": 23707
},
{
"alliance_id": 99009547,
"character_id": 95748579,
"corporation_id": 98618666,
"damage_done": 0,
"final_blow": false,
"security_status": -7.9,
"ship_type_id": 643,
"weapon_type_id": 16497
},
{
"alliance_id": 99006941,
"character_id": 2114899882,
"corporation_id": 98416134,
"damage_done": 0,
"final_blow": false,
"security_status": -1.7,
"ship_type_id": 12017,
"weapon_type_id": 484
},
{
"alliance_id": 99006941,
"character_id": 95624225,
"corporation_id": 98416134,
"damage_done": 0,
"final_blow": false,
"security_status": -9.2,
"ship_type_id": 12017,
"weapon_type_id": 37608
},
{
"alliance_id": 99006941,
"character_id": 93452185,
"corporation_id": 98416134,
"damage_done": 0,
"final_blow": false,
"security_status": -2.1,
"ship_type_id": 22474,
"weapon_type_id": 7537
},
{
"alliance_id": 99006941,
"character_id": 2114109824,
"corporation_id": 98416134,
"damage_done": 0,
"final_blow": false,
"security_status": -2.1,
"ship_type_id": 49713,
"weapon_type_id": 484
},
{
"alliance_id": 99006941,
"character_id": 2113100583,
"corporation_id": 98416134,
"damage_done": 0,
"final_blow": false,
"security_status": -0.2,
"ship_type_id": 49713,
"weapon_type_id": 484
}
],
"killmail_id": 81973979,
"killmail_time": "2020-03-01T13:10:55Z",
"solar_system_id": 30002537,
"victim": {
"alliance_id": 99009333,
"character_id": 93330670,
"corporation_id": 98267621,
"damage_taken": 1127412,
"items": [
{
"flag": 5,
"item_type_id": 41332,
"quantity_destroyed": 3,
"singleton": 0
},
{
"flag": 5,
"item_type_id": 41332,
"quantity_dropped": 3,
"singleton": 0
},
{
"flag": 27,
"item_type_id": 20847,
"quantity_destroyed": 1,
"singleton": 0
},
{
"flag": 155,
"item_type_id": 33474,
"quantity_destroyed": 1,
"singleton": 0
},
{
"flag": 18,
"item_type_id": 2048,
"quantity_destroyed": 1,
"singleton": 0
},
{
"flag": 28,
"item_type_id": 4292,
"quantity_destroyed": 1,
"singleton": 0
},
{
"flag": 5,
"item_type_id": 29001,
"quantity_dropped": 1,
"singleton": 0
},
{
"flag": 22,
"item_type_id": 41218,
"quantity_dropped": 1,
"singleton": 0
},
{
"flag": 133,
"item_type_id": 16275,
"quantity_destroyed": 1125,
"singleton": 0
},
{
"flag": 5,
"item_type_id": 41330,
"quantity_destroyed": 1,
"singleton": 0
},
{
"flag": 5,
"item_type_id": 41330,
"quantity_dropped": 4,
"singleton": 0
},
{
"flag": 94,
"item_type_id": 31452,
"quantity_destroyed": 1,
"singleton": 0
},
{
"flag": 5,
"item_type_id": 20028,
"quantity_destroyed": 1,
"singleton": 0
},
{
"flag": 5,
"item_type_id": 20028,
"quantity_dropped": 2,
"singleton": 0
},
{
"flag": 155,
"item_type_id": 41489,
"quantity_dropped": 48,
"singleton": 0
},
{
"flag": 13,
"item_type_id": 18708,
"quantity_destroyed": 1,
"singleton": 0
},
{
"flag": 21,
"item_type_id": 1978,
"quantity_destroyed": 1,
"singleton": 0
},
{
"flag": 5,
"item_type_id": 20026,
"quantity_destroyed": 3,
"singleton": 0
},
{
"flag": 29,
"item_type_id": 20847,
"quantity_destroyed": 1,
"singleton": 0
},
{
"flag": 21,
"item_type_id": 29001,
"quantity_destroyed": 1,
"singleton": 0
},
{
"flag": 155,
"item_type_id": 16275,
"quantity_dropped": 1250,
"singleton": 0
},
{
"flag": 155,
"item_type_id": 16299,
"quantity_destroyed": 6,
"singleton": 0
},
{
"flag": 155,
"item_type_id": 16299,
"quantity_dropped": 2,
"singleton": 0
},
{
"flag": 5,
"item_type_id": 41489,
"quantity_dropped": 12,
"singleton": 0
},
{
"flag": 29,
"item_type_id": 37298,
"quantity_dropped": 1,
"singleton": 0
},
{
"flag": 17,
"item_type_id": 40351,
"quantity_destroyed": 1,
"singleton": 0
},
{
"flag": 19,
"item_type_id": 41491,
"quantity_destroyed": 1,
"singleton": 0
},
{
"flag": 12,
"item_type_id": 2364,
"quantity_dropped": 1,
"singleton": 0
},
{
"flag": 5,
"item_type_id": 20030,
"quantity_dropped": 3,
"singleton": 0
},
{
"flag": 14,
"item_type_id": 18708,
"quantity_destroyed": 1,
"singleton": 0
},
{
"flag": 5,
"item_type_id": 28999,
"quantity_destroyed": 1,
"singleton": 0
},
{
"flag": 93,
"item_type_id": 30993,
"quantity_destroyed": 1,
"singleton": 0
},
{
"flag": 11,
"item_type_id": 2364,
"quantity_destroyed": 1,
"singleton": 0
},
{
"flag": 20,
"item_type_id": 29001,
"quantity_dropped": 1,
"singleton": 0
},
{
"flag": 15,
"item_type_id": 40351,
"quantity_destroyed": 1,
"singleton": 0
},
{
"flag": 19,
"item_type_id": 41489,
"quantity_dropped": 4,
"singleton": 0
},
{
"flag": 5,
"item_type_id": 21254,
"quantity_destroyed": 1,
"singleton": 0
},
{
"flag": 5,
"item_type_id": 21254,
"quantity_dropped": 1,
"singleton": 0
},
{
"flag": 5,
"item_type_id": 20032,
"quantity_destroyed": 3,
"singleton": 0
},
{
"flag": 5,
"item_type_id": 20032,
"quantity_dropped": 1,
"singleton": 0
},
{
"flag": 16,
"item_type_id": 40351,
"quantity_dropped": 1,
"singleton": 0
},
{
"flag": 5,
"item_type_id": 20843,
"quantity_dropped": 3,
"singleton": 0
},
{
"flag": 31,
"item_type_id": 37298,
"quantity_destroyed": 1,
"singleton": 0
},
{
"flag": 5,
"item_type_id": 20845,
"quantity_destroyed": 3,
"singleton": 0
},
{
"flag": 92,
"item_type_id": 30993,
"quantity_destroyed": 1,
"singleton": 0
},
{
"flag": 31,
"item_type_id": 20847,
"quantity_destroyed": 1,
"singleton": 0
},
{
"flag": 133,
"item_type_id": 16274,
"quantity_dropped": 141666,
"singleton": 0
},
{
"flag": 20,
"item_type_id": 1978,
"quantity_destroyed": 1,
"singleton": 0
},
{
"flag": 5,
"item_type_id": 20847,
"quantity_destroyed": 6,
"singleton": 0
},
{
"flag": 5,
"item_type_id": 20847,
"quantity_dropped": 3,
"singleton": 0
},
{
"flag": 5,
"item_type_id": 21246,
"quantity_destroyed": 1,
"singleton": 0
},
{
"flag": 27,
"item_type_id": 37298,
"quantity_dropped": 1,
"singleton": 0
},
{
"flag": 90,
"item_type_id": 585,
"items": [
{
"flag": 94,
"item_type_id": 31159,
"quantity_destroyed": 1,
"singleton": 0
},
{
"flag": 20,
"item_type_id": 3831,
"quantity_destroyed": 1,
"singleton": 0
},
{
"flag": 93,
"item_type_id": 31159,
"quantity_destroyed": 1,
"singleton": 0
},
{
"flag": 22,
"item_type_id": 9568,
"quantity_destroyed": 1,
"singleton": 0
},
{
"flag": 12,
"item_type_id": 1405,
"quantity_destroyed": 1,
"singleton": 0
},
{
"flag": 92,
"item_type_id": 31159,
"quantity_destroyed": 1,
"singleton": 0
},
{
"flag": 21,
"item_type_id": 2553,
"quantity_destroyed": 1,
"singleton": 0
},
{
"flag": 11,
"item_type_id": 1405,
"quantity_destroyed": 1,
"singleton": 0
},
{
"flag": 19,
"item_type_id": 5971,
"quantity_destroyed": 1,
"singleton": 0
}
],
"quantity_destroyed": 1,
"singleton": 0
}
],
"position": {
"x": 55026869426.47358,
"y": 7310382040.828209,
"z": -163690355689.8978
},
"ship_type_id": 19720
}
}

View File

@@ -0,0 +1,15 @@
[
{
"killmail_id": 81973979,
"zkb": {
"locationID": 60004816,
"hash": "e88a5fa7f342fa658ebe74a055b7679e28b05628",
"fittedValue": 1532365686.21,
"totalValue": 3177859026.86,
"points": 1,
"npc": false,
"solo": false,
"awox": false
}
}
]

View File

@@ -8,7 +8,7 @@ from django.contrib.humanize.templatetags.humanize import intcomma
from django.http import JsonResponse, Http404
from django.shortcuts import render, redirect, get_object_or_404
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from django.db.models import Sum
from allianceauth.authentication.decorators import permissions_required
from allianceauth.eveonline.providers import provider

View File

@@ -1,4 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=https://github.com/ErikKalkoken/filterDropDown/blob/master/js/filterDropDown.min.js
HostUrl=https://raw.githubusercontent.com/ErikKalkoken/filterDropDown/master/js/filterDropDown.min.js

View File

@@ -1,18 +0,0 @@
{% extends "allianceauth/base.html" %}
{% load i18n %}
{% block page_title %}{% trans "Help" %}{% endblock page_title %}
{% block content %}
<div class="col-lg-12">
<h1 class="page-header text-center">{% trans "Help" %}</h1>
<div class="container-fluid">
<div class="embed-responsive embed-responsive-16by9">
<iframe class="embed-responsive-item" src="https://allianceauth.readthedocs.io/en/latest/features/"></iframe>
</div>
</div>
</div>
{% endblock content %}

View File

@@ -26,15 +26,7 @@
{% endif %}
{% menu_items %}
{% if user.is_superuser %}
<li>
<a class="{% navactive request 'authentication:help' %}"
href="{% url 'authentication:help' %}">
<i class="fa fa-question fa-fw"></i> {% trans "Help" %}
</a>
</li>
{% endif %}
</ul>
</div>
</div>

View File

@@ -39,6 +39,13 @@
{% else %}
<li><a href="{% url 'authentication:login' %}">{% trans "Login" %}</a></li>
{% endif %}
{% if user.is_superuser %}
<li>
<a class="navbar-brand" style="margin-left: -4px;" href="https://allianceauth.readthedocs.io/" target="_blank">
<i class="fa fa-question-circle fa-fw"></i>
</a>
</li>
{% endif %}
</ul>
<form id="f-lang-select" class="navbar-form navbar-right" action="{% url 'set_language' %}" method="post">
{% csrf_token %}

View File

@@ -1,27 +1,45 @@
from allianceauth.authentication.models import UserProfile, State, get_guest_state
from allianceauth.authentication.signals import state_member_alliances_changed, state_member_characters_changed, \
state_member_corporations_changed, state_saved
from django.contrib.auth.models import User, Group
from django.contrib.auth.models import User, Group, Permission
from django.db.models.signals import m2m_changed, pre_save, post_save
from django.test import TestCase
from esi.models import Token
from allianceauth.eveonline.models import EveCharacter
from allianceauth.services.signals import m2m_changed_group_permissions, m2m_changed_user_permissions, \
m2m_changed_state_permissions
from allianceauth.services.signals import m2m_changed_user_groups, disable_services_on_inactive
from esi.models import Token
from allianceauth.authentication.models import (
UserProfile, State, get_guest_state
)
from allianceauth.eveonline.models import EveCharacter
from allianceauth.authentication.signals import (
state_member_alliances_changed,
state_member_characters_changed,
state_member_corporations_changed,
state_saved,
reassess_on_profile_save,
assign_state_on_active_change,
check_state_on_character_update
)
from allianceauth.services.signals import (
m2m_changed_group_permissions,
m2m_changed_user_permissions,
m2m_changed_state_permissions,
m2m_changed_user_groups, disable_services_on_inactive
)
class AuthUtils:
def __init__(self):
pass
"""Utilities for making it easier to create tests for Alliance Auth"""
@staticmethod
def _create_user(username):
def _create_user(username):
return User.objects.create(username=username)
@classmethod
def create_user(cls, username, disconnect_signals=False):
"""create a new user
username: Name of the user
disconnect_signals: whether to run process without signals
"""
if disconnect_signals:
cls.disconnect_signals()
user = cls._create_user(username)
@@ -95,6 +113,11 @@ class AuthUtils:
m2m_changed.disconnect(state_member_characters_changed, sender=State.member_characters.through)
m2m_changed.disconnect(state_member_alliances_changed, sender=State.member_alliances.through)
post_save.disconnect(state_saved, sender=State)
post_save.disconnect(reassess_on_profile_save, sender=UserProfile)
pre_save.disconnect(assign_state_on_active_change, sender=User)
post_save.disconnect(
check_state_on_character_update, sender=EveCharacter
)
@classmethod
def connect_signals(cls):
@@ -107,6 +130,9 @@ class AuthUtils:
m2m_changed.connect(state_member_characters_changed, sender=State.member_characters.through)
m2m_changed.connect(state_member_alliances_changed, sender=State.member_alliances.through)
post_save.connect(state_saved, sender=State)
post_save.connect(reassess_on_profile_save, sender=UserProfile)
pre_save.connect(assign_state_on_active_change, sender=User)
post_save.connect(check_state_on_character_update, sender=EveCharacter)
@classmethod
def add_main_character(cls, user, name, character_id, corp_id='', corp_name='', corp_ticker='', alliance_id='',
@@ -122,6 +148,40 @@ class AuthUtils:
)
UserProfile.objects.update_or_create(user=user, defaults={'main_character': char})
@classmethod
def add_main_character_2(
cls,
user,
name,
character_id,
corp_id='',
corp_name='',
corp_ticker='',
alliance_id='',
alliance_name='',
disconnect_signals=False
):
"""new version that works in all cases"""
if disconnect_signals:
cls.disconnect_signals()
char = EveCharacter.objects.create(
character_id=character_id,
character_name=name,
corporation_id=corp_id,
corporation_name=corp_name,
corporation_ticker=corp_ticker,
alliance_id=alliance_id,
alliance_name=alliance_name,
)
user.profile.main_character = char
user.profile.save()
if disconnect_signals:
cls.connect_signals()
return char
@classmethod
def add_permissions_to_groups(cls, perms, groups, disconnect_signals=True):
if disconnect_signals:
@@ -130,14 +190,67 @@ class AuthUtils:
for group in groups:
for perm in perms:
group.permissions.add(perm)
group = Group.objects.get(pk=group.pk) # reload permission cache
if disconnect_signals:
cls.connect_signals()
@classmethod
def add_permissions_to_state(cls, perms, states, disconnect_signals=True):
return cls.add_permissions_to_groups(perms, states, disconnect_signals=disconnect_signals)
return cls.add_permissions_to_groups(
perms, states, disconnect_signals=disconnect_signals
)
@classmethod
def add_permissions_to_user(cls, perms, user, disconnect_signals=True):
"""add list of permissions to user
perms: list of Permission objects
user: user object
disconnect_signals: whether to run process without signals
"""
if disconnect_signals:
cls.disconnect_signals()
for perm in perms:
user.user_permissions.add(perm)
user = User.objects.get(pk=user.pk) # reload permission cache
if disconnect_signals:
cls.connect_signals()
@classmethod
def add_permission_to_user_by_name(
cls, perm, user, disconnect_signals=True
):
"""returns permission specified by qualified name
perm: Permission name as 'app_label.codename'
user: user object
disconnect_signals: whether to run process without signals
"""
p = cls.get_permission_by_name(perm)
cls.add_permissions_to_user([p], user, disconnect_signals)
@staticmethod
def get_permission_by_name(perm: str) -> Permission:
"""returns permission specified by qualified name
perm: Permission name as 'app_label.codename'
Returns: Permission object or throws exception if not found
"""
perm_parts = perm.split('.')
if len(perm_parts) != 2:
raise ValueError('Invalid format for permission name')
return Permission.objects.get(
content_type__app_label=perm_parts[0], codename=perm_parts[1]
)
class BaseViewTestCase(TestCase):
def setUp(self):

View File

@@ -0,0 +1,60 @@
from unittest import mock
from django.contrib.auth.models import User, Group, Permission
from django.test import TestCase
from allianceauth.eveonline.models import (
EveCorporationInfo, EveAllianceInfo, EveCharacter
)
from .auth_utils import AuthUtils
class TestAuthUtils(TestCase):
def test_can_create_user(self):
user = AuthUtils.create_user('Bruce Wayne')
self.assertTrue(User.objects.filter(username='Bruce Wayne').exists())
def test_can_add_main_character_2(self):
user = AuthUtils.create_user('Bruce Wayne')
character = AuthUtils.add_main_character_2(
user,
name='Bruce Wayne',
character_id=1001,
corp_id=2001,
corp_name='Wayne Technologies',
corp_ticker='WYT',
alliance_id=3001,
alliance_name='Wayne Enterprises'
)
expected = character
self.assertEqual(user.profile.main_character, expected)
def test_can_add_permission_to_group(self):
group = Group.objects.create(name='Dummy Group')
p = AuthUtils.get_permission_by_name('auth.group_management')
AuthUtils.add_permissions_to_groups([p], [group])
self.assertTrue(group.permissions.filter(pk=p.pk).exists())
def test_can_add_permission_to_user_by_name(self):
user = AuthUtils.create_user('Bruce Wayne')
AuthUtils.add_permission_to_user_by_name(
'auth.timer_management', user
)
self.assertTrue(user.has_perm('auth.timer_management'))
class TestGetPermissionByName(TestCase):
def test_can_get_permission_by_name(self):
expected = Permission.objects.get(
content_type__app_label='auth', codename='timer_management'
)
self.assertEqual(
AuthUtils.get_permission_by_name('auth.timer_management'), expected
)
def test_raises_exception_on_invalid_permission_format(self):
with self.assertRaises(ValueError):
AuthUtils.get_permission_by_name('timer_management')

View File

@@ -1,13 +1,15 @@
from django.views.generic.base import View
from django.http import HttpResponseRedirect
from django.shortcuts import redirect
from django.contrib import messages
class NightModeRedirectView(View):
SESSION_VAR = 'NIGHT_MODE'
SESSION_VAR = "NIGHT_MODE"
def get(self, request, *args, **kwargs):
request.session[self.SESSION_VAR] = not self.night_mode_state(request)
return HttpResponseRedirect(request.GET.get('next', '/'))
return HttpResponseRedirect(request.GET.get("next", "/"))
@classmethod
def night_mode_state(cls, request):
@@ -17,3 +19,39 @@ class NightModeRedirectView(View):
# Session is middleware
# Sometimes request wont have a session attribute
return False
def Generic500Redirect(request):
messages.error(
request,
"Auth encountered an error processing your request, please try again. "
"If the error persists, please contact the administrators. (500 Internal Server Error)",
)
return redirect("authentication:dashboard")
def Generic404Redirect(request, exception):
messages.error(
request,
"Page does not exist. If you believe this is in error please contact the administrators. "
"(404 Page Not Found)",
)
return redirect("authentication:dashboard")
def Generic403Redirect(request, exception):
messages.error(
request,
"You do not have permission to access the requested page. "
"If you believe this is in error please contact the administrators. (403 Permission Denied)",
)
return redirect("authentication:dashboard")
def Generic400Redirect(request, exception):
messages.error(
request,
"Auth encountered an error processing your request, please try again. "
"If the error persists, please contact the administrators. (400 Bad Request)",
)
return redirect("authentication:dashboard")

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

View File

@@ -1,4 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=https://i.imgur.com/6dXZ5BH.png
HostUrl=https://i.imgur.com/6dXZ5BH.png

View File

@@ -1,4 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=https://i.imgur.com/Er1g02v.png
HostUrl=https://i.imgur.com/Er1g02v.png

View File

@@ -18,7 +18,10 @@
#
import os
import sys
sys.path.insert(0, os.path.abspath('.'))
import django
sys.path.insert(0, os.path.abspath('..'))
os.environ['DJANGO_SETTINGS_MODULE'] = 'tests.settings_all'
django.setup()
# on_rtd is whether we are on readthedocs.org, this line of code grabbed from docs.readthedocs.org
on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
@@ -38,7 +41,9 @@ from recommonmark.transform import AutoStructify
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = []
extensions = [
'sphinx.ext.autodoc',
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
@@ -148,6 +153,9 @@ man_pages = [
[author], 1)
]
# -- Options for autodoc -------------------------------------------------
add_module_names = False
# -- Options for Texinfo output -------------------------------------------

View File

@@ -9,4 +9,5 @@ This section describes how to extend **Alliance Auth** with custom apps and serv
integrating-services
menu-hooks
url-hooks
logging
```

View File

@@ -0,0 +1,15 @@
# Logging from Custom Apps
Alliance Auth provides a logger for use with custom apps to make everyone's life a little easier.
## Using the Extensions Logger
AllianceAuth provides a helper function to get the logger for the current module to reduce the amount of
code you need to write.
```python
from allianceauth.services.hooks import get_extension_logger
logger = get_extension_logger(__name__)
```
This works by creating a child logger of the extension logger which propagates all log entries
to the parent (extensions) logger.

View File

@@ -0,0 +1,463 @@
# Development on Windows 10 with WSL and Visual Studio Code
This document describes step-by-step how to setup a complete development environment for Alliance Auth apps on Windows 10 with Windows Subsystem for Linux (WSL) and Visual Studio Code.
The main benefit of this setup is that it runs all services and code in the native Linux environment (WSL) and at the same time can be full controlled from within a comfortable Windows IDE (Visual Studio Code) including code debugging.
In addition all tools described in this guide are open source or free software.
```eval_rst
.. hint::
This guide is meant for development purposes only and not for installing AA in a production environment. For production installation please see chapter **Installation**.
```
## Overview
The development environment consists of the following components:
- Visual Studio Code with Remote WSL and Python extension
- WSL with Ubunutu 18.04. LTS
- Python 3.6 environment on WSL
- MySQL server on WSL
- Redis on WSL
- Alliance Auth on WSL
- Celery on WSL
We will use the build-in Django development webserver, so we don't need to setup a WSGI server or a web server.
## Requirement
The only requirement is a PC with Windows 10 and Internet connection in order to download the additional software components.
## Windows installation
### Windows Subsystem for Linux
- Install from here: [Microsoft docs](https://docs.microsoft.com/en-us/windows/wsl/install-win10)
- Choose Ubuntu 18.04. LTS
### Visual Studio Code
- Install from here: [VSC Download](https://code.visualstudio.com/Download)
- Open the app and install the following VSC extensions:
- Remote WSL
- Connect to WSL. This will automatically install the VSC server on the VSC server for WSL
- Once connected to WSL install the Python extension on the WSL side
## WSL Installation
Open a WSL bash and update all software packets:
```bash
sudo apt update && sudo apt upgrade -y
```
### Install Tools
```bash
sudo apt-get install build-essential
sudo apt-get install gettext
```
### Install Python
For AA we want to develop with Python 3.6, because that provides the maximum compatibility with today's AA installations. This also happens to be the default Python 3 version for Ubuntu 18.04. at the point of this writing.
```eval_rst
.. hint::
To check your system's Python 3 version you can enter: ``python3 --version``
```
```eval_rst
.. note::
Should your Ubuntu come with a newer version of Python we recommend to still setup your dev environment with the oldest Python 3 version supported by AA, e.g Python 3.6
You an check out this `page <https://askubuntu.com/questions/682869/how-do-i-install-a-different-python-version-using-apt-get/1195153>`_ on how to install additional Python versions on Ubuntu.
```
Use the following command to install Python 3 with all required libraries with the default version:
```bash
sudo apt-get install python3 python3-dev python3-venv python3-setuptools python3-pip python-pip
```
### Installing the DBMS
Install MySQL and required libraries with the following command:
```bash
sudo apt-get install mysql-server mysql-client libmysqlclient-dev
```
```eval_rst
.. note::
We chose to use MySQL instead of MariaDB, because the standard version of MariaDB that comes with this Ubuntu distribution will not work with AA.
```
We need to apply a permission fix to mysql or you will get a warning with every startup:
```bash
sudo usermod -d /var/lib/mysql/ mysql
```
Start the mysql server
```bash
sudo service mysql start
```
Create database and user for AA
```bash
sudo mysql -u root
```
```sql
CREATE USER 'aa_dev'@'localhost' IDENTIFIED BY 'PASSWORD';
CREATE DATABASE aa_dev CHARACTER SET utf8mb4;
GRANT ALL PRIVILEGES ON aa_dev . * TO 'aa_dev'@'localhost';
CREATE DATABASE test_aa_dev CHARACTER SET utf8mb4;
GRANT ALL PRIVILEGES ON test_aa_dev . * TO 'aa_dev'@'localhost';
exit;
```
Add timezone info to mysql
```bash
sudo mysql_tzinfo_to_sql /usr/share/zoneinfo | sudo mysql -u root mysql
```
### Install redis and other tools
```bash
sudo apt-get install unzip git redis-server curl libssl-dev libbz2-dev libffi-dev
```
Start redis
```bash
sudo redis-server --daemonize yes
```
```eval_rst
.. note::
WSL does not have an init.d service, so it will not automatically start your services such as MySQL and Redis when you boot your Windows machine. For convenience we recommend putting the commands for starting these services in a bash script. Here is an example: ::
#/bin/bash
# start services for AA dev
sudo service mysql start
sudo redis-server --daemonize yes
In addition it is possible to configure Windows to automatically start WSL services, but that procedure goes beyond the scopes of this guide.
```
### Setup dev folder on WSL
Setup your folders on WSL bash for your dev project. Our approach will setup one AA project with one venv and multiple apps running under the same AA project, but each in their own folder and git.
A good location for setting up this folder structure is your home folder or a subfolder of your home:
```text
~/aa-dev
|- venv
|- myauth
|- my_app_1
|- my_app_2
|- ...
```
Following this approach you can also setup additional AA projects, e.g. aa-dev-2, aa-dev-3 if needed.
Create the root folder aa-dev.
### setup virtual Python environment for aa-dev
Create the virtual environment. Run this in your aa-dev folder:
```bash
python3 -m venv venv
```
And activate your venv:
```bash
source venv/bin/activate
```
### install Python packages
```bash
pip install --upgrade pip
pip install wheel
```
## Alliance Auth installation
## Install and create AA instance
```bash
pip install allianceauth
```
Now we are ready to setup our AA instance. Make sure to run this command in your aa-dev folder:
```bash
allianceauth start myauth
```
Next we will setup our VSC project for aa-dev by starting it directly from the WSL bash:
```bash
code .
```
First you want to make sure exclude the venv folder from VSC as follows:
Open settings and go to Files:Exclude
Add pattern: `**/venv`
### Update settings
Open the settings file with VSC. Its under `myauth/myauth/settings/local.py`
Make sure to have the settings of your Eve Online app ready.
Turn on DEBUG mode to ensure your static files get served by Django:
```python
DEBUG = True
```
Update name, user and password of your DATABASE configuration.
```python
DATABASES['default'] = {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'aa_dev',
'USER': 'aa_dev',
'PASSWORD': 'PASSWORD',
'HOST': '127.0.0.1',
'PORT': '3306',
'OPTIONS': {'charset': 'utf8mb4'},
}
```
For the Eve Online related setup you need to create a SSO app on the developer site:
- Create your Eve Online SSO App on the [Eve Online developer site](https://developers.eveonline.com/)
- Add all ESI scopes
- Set callback URL to: `http://localhost:8000/sso/callback`
Then update local.py with your settings:
```python
ESI_SSO_CLIENT_ID = 'YOUR-ID'
ESI_SSO_CLIENT_SECRET = 'YOUR_SECRET'
ESI_SSO_CALLBACK_URL = 'http://localhost:8000/sso/callback'
```
Disable email registration:
```python
REGISTRATION_VERIFY_EMAIL = False
```
### Migrations and superuser
Before we can start AA we need to run migrations:
```bash
cd myauth
python manage.py migrate
```
We also need to create a superuser for our AA installation:
```bash
python /home/allianceserver/myauth/manage.py createsuperuser
```
## Running Alliance Auth
## AA instance
We are now ready to run out AA instance with the following command:
```bash
python manage.py runserver
```
Once running you can access your auth site on the browser under `http://localhost:8000`. Or the admin site under `http://localhost:8000/admin`
```eval_rst
.. hint::
You can start your AA server directly from a terminal window in VSC or with a VSC debug config (see chapter about debugging for details).
```
```eval_rst
.. note::
**Debug vs. Non-Debug mode**
Usually it is best to run your dev AA instance in debug mode, so you get all the detailed error messages that helps a lot for finding errors. But there might be cases where you want to test features that do not exist in debug mode (e.g. error pages) or just want to see how your app behaves in non-debug / production mode.
When you turn off debug mode you will see a problem though: Your pages will not render correctly. The reason is that Django will stop serving your static files in production mode and expect you to serve them from a real web server. Luckily, there is an option that forces Django to continue serving your static files directly even when not in debug mode. Just start your server with the following option: ``python manage.py runserver --insecure``
```
### Celery
In addition you can start a celery worker instance for myauth. For development purposed it makes sense to only start one instance and add some additional logging.
This can be done from the command line with the following command in the myauth folder (where manage.py is located):
```bash
celery -E -A myauth worker -l info -P solo
```
Same as AA itself you can start Celery from any terminal session, from a terminal window within VSC or as a debug config in VSC (see chapter about debugging for details). For convenience we recommend starting Celery as debug config.
## Debugging setup
To be able to debug your code you need to add debugging configuration to VSC. At least one for AA and one for celery.
### Breakpoints
By default VSC will break on any uncaught exception. Since every error raised by your tests will cause an uncaught exception we recommend to deactivate this feature.
To deactivate open click on the debug icon to switch to the debug view. Then un-check "Uncaught Exceptions" on the breakpoints window.
### AA debug config
In VSC click on Debug / Add Configuration and choose "Django". Should Django not appear as option make sure to first open a Django file (e.g. the local.py settings) to help VSC detect that you are using Django.
The result should look something like this:
```python
{
"name": "Python: Django",
"type": "python",
"request": "launch",
"program": "${workspaceFolder}/myauth/manage.py",
"args": [
"runserver",
"--noreload"
],
"django": true
}
```
### Debug celery
For celery we need another debug config, so that we can run it in parallel to our AA instance.
Here is an example debug config for Celery:
```javascript
{
"name": "Python: Celery",
"type": "python",
"request": "launch",
"module": "celery",
"cwd": "${workspaceFolder}/myauth",
"console": "integratedTerminal",
"args": [
"-E",
"-A",
"myauth",
"worker",
"-l",
"info",
"-P",
"solo",
],
"django": true
},
```
### Debug config for unit tests
Finally it makes sense to have a dedicated debug config for running unit tests. Here is an example config for running all tests of the app `example`.
```javascript
{
"name": "Python: myauth unit tests",
"type": "python",
"request": "launch",
"program": "${workspaceFolder}/myauth/manage.py",
"args": [
"test",
"-v 2",
"--keepdb",
"--debug-mode",
"--failfast",
"example",
],
"django": true
},
```
You can also specify to run just a part of your test suite down to a test method. Just give the full path to the test you want to run, e.g. `example.test.test_models.TestDemoModel.test_this_method`
### Debugging normal python scripts
Finally you may also want to have a debug config to debug a non-Django Python script:
```javascript
{
"name": "Python: Current File",
"type": "python",
"request": "launch",
"program": "${file}",
"console": "integratedTerminal"
},
```
## Additional tools
The following additional tools are very helpful when developing for AA.
### Code Spell Checker
Typos in your user facing comments can be quite embarrassing. This spell checker helps you avoid them.
Install from here: [Code Spell Checker](https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker)
### DBeaver
DBeaver is a free universal database tool and works with many different kinds of databases include MySQL. It can be installed on Windows 10 and will be able to help manage your MySQL databases running on WSL.
Install from here. [DBeaver](https://dbeaver.io/)
### django-extensions
[django-extensions](https://django-extensions.readthedocs.io/en/latest/) is a swiss army knife for django developers with adds a lot of very useful features to your Django site. Here are a few highlights:
- shell_plus - An enhanced version of the Django shell. It will auto-load all your models at startup so you don't have to import anything and can use them right away.
- graph_models - Creates a dependency graph of Django models. Visualizing a model dependency structure can be very useful for trying to understand how an existing Django app works, or e.g. how all the AA models work together.
- runserver_plus - The standard runserver stuff but with the Werkzeug debugger baked in. This is a must have for any serious debugging.
## Adding apps for development
The idea behind the particular folder structure of aa-dev is to have each and every app in its own folder and git repo. To integrate them with the AA instance they need to be installed once using the -e option that enabled editing of the package. And then added to the INSTALLED_APPS settings.
To demonstrate let's add the example plugin to our environment.
Open a WSL bash and navigate to the aa-dev folder. Make sure you have activate your virtual environment. (`source venv/bin/activate`)
Run these commands:
```bash
git clone https://gitlab.com/ErikKalkoken/allianceauth-example-plugin.git
pip install -e allianceauth-example-plugin
```
Add `'example'` to INSTALLED_APPS in your `local.py` settings.
Run migrations and restart your AA server, e.g.:
```bash
cd myauth
python manage.py migrate
```

View File

@@ -0,0 +1,10 @@
# Setup dev environment for AA
Here you find guides on how to setup your development environment for AA.
```eval_rst
.. toctree::
:maxdepth: 1
aa-dev-setup-wsl-vsc-v2
```

View File

@@ -8,4 +8,6 @@
custom/index
aa_core/index
dev_setup/index
tech_docu/index
```

View File

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

View File

@@ -0,0 +1,30 @@
===============================
evelinks
===============================
This package generates profile URLs for eve entities on 3rd party websites like evewho and zKillboard.
Location: ``allianceauth.eveonline.evelinks``
dotlan
===============
.. automodule:: allianceauth.eveonline.evelinks.dotlan
:members:
:undoc-members:
eveho
==============
.. automodule:: allianceauth.eveonline.evelinks.evewho
:members:
:undoc-members:
zkillboard
===================
.. automodule:: allianceauth.eveonline.evelinks.zkillboard
:members:
:undoc-members:

View File

@@ -0,0 +1,15 @@
======================
eveonline
======================
The eveonline package provides models for commonly used Eve Online entities like characters, corporations and alliances. All models have the ability to be loaded from ESI.
Location: ``allianceauth.eveonline``
models
======
.. automodule:: allianceauth.eveonline.models
:members:
:exclude-members: objects, provider
:undoc-members:

View File

@@ -0,0 +1,13 @@
# API
To reduce redundancy and help speed up development we encourage developers to utilize the following packages when developing apps for Alliance Auth.
```eval_rst
.. toctree::
:maxdepth: 1
esi
evelinks
eveonline
testutils
```

View File

@@ -0,0 +1,14 @@
=============================
tests
=============================
Here you find utility functions and classes, which can help speed up writing test cases for AA.
Location: ``allianceauth.tests.auth_utils``
auth_utils
===========
.. automodule:: allianceauth.tests.auth_utils
:members:
:undoc-members:

View File

@@ -0,0 +1,204 @@
# Celery FAQ
**Alliance Auth** uses Celery for asynchronous task management. This page aims to give developers some guidance on how to use Celery when developing apps for Alliance Auth.
For a complete documentation of Celery please refer to the [official Celery documentation](http://docs.celeryproject.org/en/latest/index.html).
## When should I use Celery in my app?
There are two main cases for using celery. Long duration of a process and recurrence of a process.
### Duration
Alliance Auth is an online web application and as such the user expects fast and immediate responses to any of his clicks or actions. Same as with any other good web site. Good response times are measures in ms and a user will perceive everything that takes longer than 1 sec as an interruption of his flow of thought (see also [Response Times: The 3 Important Limits](https://www.nngroup.com/articles/response-times-3-important-limits/)).
As a rule of thumb we therefore recommend to use celery tasks for every process that can take longer than 1 sec to complete (also think about how long your process might take with large amounts of data).
```eval_rst
.. note::
Another solution for dealing with long response time in particular when loading pages is to load parts of a page asynchronously, for example with AJAX.
```
### Recurrence
Another case for using celery tasks is when you need recurring execution of tasks. For example you may want to update the list of characters in a corporation from ESI every hour.
These are called periodic tasks and Alliance Auth uses [celery beat](https://docs.celeryproject.org/en/latest/userguide/periodic-tasks.html) to implement them.
## What is a celery task?
For the most part a celery task is a Python functions that is configured to be executed asynchronously and controlled by Celery. Celery tasks can be automatically retried, executed periodically, executed in work flows and much more. See the [celery docs](https://docs.celeryproject.org/en/latest/userguide/tasks.html) for a more detailed description.
## How should I use Celery in my app?
Please use the following approach to ensure your tasks are working properly with Alliance Auth:
- All tasks should be defined in a module of your app's package called `tasks.py`
- Every task is a Python function with has the `@shared_task` decorator.
- Task functions and the tasks module should be kept slim, just like views by mostly utilizing business logic defined in your models/managers.
- Tasks should always have logging, so their function and potential errors can be monitored properly
Here is an example implementation of a task:
```Python
import logging
from celery import shared_task
logger = logging.getLogger(__name__)
@shared_task
def example():
logger.info('example task started')
```
This task can then be started from any another Python module like so:
```Python
from .tasks import example
example.delay()
```
## How should I use celery tasks in the UI?
There is a well established pattern for integrating asynchronous processes in the UI, for example when the user asks your app to perform a longer running action:
1. Notify the user immediately (with a Django message) that the process for completing the action has been started and that he will receive a report once completed.
2. Start the celery task
3. Once the celery task is completed it should send a notification containing the result of the action to the user. It's important to send that notification also in case of errors.
## Can I use long running tasks?
Long running tasks are possible, but in general Celery works best with short running tasks. Therefore we strongly recommend to try and break down long running tasks into smaller tasks if possible.
If contextually possible try to break down your long running task in shorter tasks that can run in parallel.
However, many long running tasks consist of several smaller processes that need to run one after the other. For example you may have a loop where you perform the same action on hundreds of objects. In those cases you can define each of the smaller processes as it's own task and then link them together, so that they are run one after the other. That is called chaining in Celery and is the preferred approach for implementing long running processes.
Example implementation for a celery chain:
```Python
import logging
from celery import shared_task, chain
logger = logging.getLogger(__name__)
@shared_task
def example():
logger.info('example task')
@shared_task
def long_runner():
logger.info('started long runner')
my_tasks = list()
for _ in range(10):
task_signature = example.si()
my_task.append(task_signature)
chain(my_tasks).delay()
```
In this example we fist add 10 example tasks that need to run one after the other to a list. This can be done by creating a so called signature for a task. Those signature are a kind of wrapper for tasks and can be used in various ways to compose work flow for tasks.
The list of task signatures is then converted to a chain and started asynchronously.
```eval_rst
.. hint::
In our example we use ``si()``, which is a shortcut for "immutable signatures" and prevents us from having to deal with result sharing between tasks.
For more information on signature and work flows see the official documentation on `Canvas <https://docs.celeryproject.org/en/latest/userguide/canvas.html>`_.
In this context please note that Alliance Auth currently only supports chaining, because all other variants require a so called results back, which Alliance Auth does not have.
```
## How can I define periodic tasks for my app?
Periodic tasks are normal celery tasks which are added the scheduler for periodic execution. The convention for defining periodic tasks for an app is to define them in the local settings. So user will need to add those settings manually to his local settings during the installation process.
Example setting:
```Python
CELERYBEAT_SCHEDULE['structures_update_all_structures'] = {
'task': 'structures.tasks.update_all_structures',
'schedule': crontab(minute='*/30'),
}
```
- `structures_update_all_structures` is the name of the scheduling entry. You can chose any name, but the convention is name of your app plus name of the task.
- `'task'`: Name of your task (full path)
- `'schedule'`: Schedule definition (see Celery documentation on [Periodic Tasks](https://docs.celeryproject.org/en/latest/userguide/periodic-tasks.html) for details)
## How can I use priorities for tasks?
In Alliance Auth we have defined task priorities from 0 - 9 as follows:
```eval_rst
====== ========= ===========
Number Priority Description
====== ========= ===========
0 Reserved Reserved for Auth and may not be used by apps
1, 2 Highest Needs to run right now
3, 4 High needs to run as soon as practical
5 Normal default priority for most tasks
6, 7 Low needs to run soonish, but is less urgent than most tasks
8, 9 Lowest not urgent, can be run whenever there is time
====== ========= ===========
```
```eval_rst
.. warning::
Please make sure to use task priorities with care and especially do not use higher priorities without a good reason. All apps including Alliance Auth share the same task queues, so using higher task priorities excessively can potentially prevent more important tasks (of other apps) from completing on time.
You also want to make sure to run use lower priorities if you have a large amount of tasks or long running tasks, which are not super urgent. (e.g. the regular update of all Eve characters from ESI runs with priority 7)
```
```eval_rst
.. hint::
If no priority is specified all tasks will be started with the default priority, which is 5.
```
To run a task with a different priority you need to specify it when starting it.
Example for starting a task with priority 3:
```Python
example.apply_async(priority=3)
```
```eval_rst
.. hint::
For defining a priority to tasks you can not use the convenient shortcut ``delay()``, but instead need to start a task with ``apply_async()``, which also requires you to pass parameters to your task function differently. Please check out the `official docs <https://docs.celeryproject.org/en/stable/reference/celery.app.task.html#celery.app.task.Task.apply_async>`_ for details.
```
## What special features should I be aware of?
Every Alliance Auth installation will come with a couple of special celery related features "out-of-the-box" that you can make use of in your apps.
### celery-once
Celery-once is a celery extension "that allows you to prevent multiple execution and queuing of celery tasks". What that means is that you can ensure that only one instance of a celery task runs at any given time. This can be useful for example if you do not want multiple instances of you task to talk to the same external service at the same time.
We use a custom backend for celery_once in Alliance Auth defined [here](https://gitlab.com/allianceauth/allianceauth/-/blob/master/allianceauth/services/tasks.py#L14)
You can import it for use like so:
```Python
from allianceauth.services.tasks import QueueOnce
```
An example of AllianceAuth's use within the `@sharedtask` decorator, can be seen [here](https://gitlab.com/allianceauth/allianceauth/-/blob/master/allianceauth/services/modules/discord/tasks.py#L62) in the discord module
You can use it like so:
```Python
@shared_task(bind=True, name='your_modules.update_task', base=QueueOnce)
```
Please see the [official documentation](hhttps://pypi.org/project/celery_once/) of celery-once for details.
### task priorities
Alliance Auth is using task priorities to enable priority based scheduling of task execution. Please see [How can I use priorities for tasks?](#how-can-i-use-priorities-for-tasks) for details.

View File

@@ -0,0 +1,5 @@
# Core models
The following diagram shows the core models of AA and Django and their relationships:
![aa_core](/_static/images/development/aa_core.png)

View File

@@ -0,0 +1,13 @@
# Developing apps
In this section you find topics useful for app developers.
```eval_rst
.. toctree::
:maxdepth: 1
api/index
celery
core_models
templatetags
```

View File

@@ -0,0 +1,16 @@
=============
Template Tags
=============
The following template tags are available to be used by all apps. To use them just load the respeetive template tag in your template like so:
.. code-block:: html
{% load evelinks %}
evelinks
========
.. automodule:: allianceauth.eveonline.templatetags.evelinks
:members:
:undoc-members:

View File

@@ -1,6 +1,6 @@
# Overview
**Alliance Auth** (AA) is a web application that helps Eve Online organizations efficiently manage access to external services and web apps.
**Alliance Auth** (AA) is a web site that helps Eve Online organizations efficiently manage access to applications and external services.
It has the following key features:
@@ -14,6 +14,4 @@ It has the following key features:
- Can be easily extended with additional services and apps. Many are provided by the [community](/features/community/index).
Here is an example how the main page of the web site looks:
![dashboard](/_static/images/features/core/dashboard/dashboard.png)
- Chinese, English, German and Spanish localization

Some files were not shown because too many files have changed in this diff Show More