diff --git a/allianceauth/groupmanagement/tests.py b/allianceauth/groupmanagement/tests.py
deleted file mode 100644
index a39b155a..00000000
--- a/allianceauth/groupmanagement/tests.py
+++ /dev/null
@@ -1 +0,0 @@
-# Create your tests here.
diff --git a/allianceauth/services/admin.py b/allianceauth/services/admin.py
new file mode 100644
index 00000000..6132d922
--- /dev/null
+++ b/allianceauth/services/admin.py
@@ -0,0 +1,20 @@
+from django.contrib import admin
+from django import forms
+from allianceauth import hooks
+from .models import NameFormatConfig
+
+
+class NameFormatConfigForm(forms.ModelForm):
+ def __init__(self, *args, **kwargs):
+ super(NameFormatConfigForm, self).__init__(*args, **kwargs)
+ SERVICE_CHOICES = [(s.name, s.name) for h in hooks.get_hooks('services_hook') for s in [h()]]
+ if self.instance.id:
+ SERVICE_CHOICES.append((self.instance.field, self.instance.field))
+ self.fields['service_name'] = forms.ChoiceField(choices=SERVICE_CHOICES)
+
+
+class NameFormatConfigAdmin(admin.ModelAdmin):
+ form = NameFormatConfigForm
+
+
+admin.site.register(NameFormatConfig, NameFormatConfigAdmin)
diff --git a/allianceauth/services/hooks.py b/allianceauth/services/hooks.py
index e4d79121..6318fdf9 100644
--- a/allianceauth/services/hooks.py
+++ b/allianceauth/services/hooks.py
@@ -1,8 +1,14 @@
from django.conf.urls import include, url
from django.template.loader import render_to_string
+from django.utils.functional import cached_property
+from django.core.exceptions import ObjectDoesNotExist
+from django.conf import settings
+from string import Formatter
from allianceauth.hooks import get_hooks
+from .models import NameFormatConfig
+
class ServicesHook:
"""
@@ -122,3 +128,77 @@ class MenuItemHook:
class UrlHook:
def __init__(self, urls, namespace, base_url):
self.include_pattern = url(base_url, include(urls, namespace=namespace))
+
+
+class NameFormatter:
+ DEFAULT_FORMAT = getattr(settings, "DEFAULT_SERVICE_NAME_FORMAT", '[{corp_ticker}] {character_name}')
+
+ def __init__(self, service, user):
+ """
+ :param service: ServicesHook of the service to generate the name for.
+ :param user: django.contrib.auth.models.User to format name for
+ """
+ self.service = service
+ self.user = user
+
+ def format_name(self):
+ """
+ :return: str Generated name
+ """
+ format_data = self.get_format_data()
+ return Formatter().vformat(self.string_formatter, args=[], kwargs=format_data)
+
+ def get_format_data(self):
+ main_char = getattr(self.user.profile, 'main_character', None)
+
+ format_data = {
+ 'character_name': getattr(main_char, 'character_name',
+ self.user.username if self._default_to_username else None),
+ 'character_id': getattr(main_char, 'character_id', None),
+ 'corp_ticker': getattr(main_char, 'corporation_ticker', None),
+ 'corp_name': getattr(main_char, 'corporation_name', None),
+ 'corp_id': getattr(main_char, 'corporation_id', None),
+ 'alliance_name': getattr(main_char, 'alliance_name', None),
+ 'alliance_id': getattr(main_char, 'alliance_id', None),
+ 'username': self.user.username,
+ }
+
+ if main_char is not None and 'alliance_ticker' in self.string_formatter:
+ # Reduces db lookups
+ try:
+ format_data['alliance_ticker'] = getattr(getattr(main_char, 'alliance', None), 'alliance_ticker', None)
+ except ObjectDoesNotExist:
+ format_data['alliance_ticker'] = None
+ return format_data
+
+ @cached_property
+ def formatter_config(self):
+ format_config = NameFormatConfig.objects.filter(service_name=self.service.name,
+ states__pk=self.user.profile.state.pk)
+
+ if format_config.exists():
+ return format_config[0]
+ return None
+
+ @cached_property
+ def string_formatter(self):
+ """
+ Try to get the config format first
+ Then the service default
+ Before finally defaulting to global default
+ :return: str
+ """
+ return getattr(self.formatter_config, 'format', self.default_formatter)
+
+ @cached_property
+ def default_formatter(self):
+ return getattr(self.service, 'name_format', self.DEFAULT_FORMAT)
+
+ @cached_property
+ def _default_to_username(self):
+ """
+ Default to a users username if they have no main character.
+ Default is True
+ :return: bool
+ """
+ return getattr(self.formatter_config, 'default_to_username', True)
diff --git a/allianceauth/services/migrations/0001_initial.py b/allianceauth/services/migrations/0001_initial.py
new file mode 100644
index 00000000..a482d393
--- /dev/null
+++ b/allianceauth/services/migrations/0001_initial.py
@@ -0,0 +1,38 @@
+# -*- 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)),
+ ],
+ ),
+ ]
diff --git a/allianceauth/services/migrations/0001_squashed_0003_delete_groupcache.py b/allianceauth/services/migrations/0001_squashed_0003_delete_groupcache.py
new file mode 100644
index 00000000..5c12be0f
--- /dev/null
+++ b/allianceauth/services/migrations/0001_squashed_0003_delete_groupcache.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.5 on 2017-10-07 03:55
+from __future__ import unicode_literals
+
+from django.conf import settings
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ replaces = [('services', '0001_initial'), ('services', '0002_auto_20161016_0135'), ('services', '0003_delete_groupcache')]
+
+ initial = True
+
+ dependencies = [
+ ('auth', '0008_alter_user_username_max_length'),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ ]
diff --git a/allianceauth/services/migrations/0002_auto_20161016_0135.py b/allianceauth/services/migrations/0002_auto_20161016_0135.py
new file mode 100644
index 00000000..68749395
--- /dev/null
+++ b/allianceauth/services/migrations/0002_auto_20161016_0135.py
@@ -0,0 +1,22 @@
+# -*- 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',
+ ),
+ ]
diff --git a/allianceauth/services/migrations/0002_nameformatter.py b/allianceauth/services/migrations/0002_nameformatter.py
new file mode 100644
index 00000000..8fb0dd45
--- /dev/null
+++ b/allianceauth/services/migrations/0002_nameformatter.py
@@ -0,0 +1,27 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.5 on 2017-10-07 06:43
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ('services', '0001_squashed_0003_delete_groupcache'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='NameFormatConfig',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('service_name', models.CharField(max_length=100)),
+ ('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(help_text='For information on constructing name formats, please see the name format documentation', max_length=100)),
+ ('states', models.ManyToManyField(help_text='States to apply this format to. You should only have one formatter for each state for each service.', to='authentication.State')),
+ ],
+ ),
+ ]
diff --git a/allianceauth/services/migrations/0003_delete_groupcache.py b/allianceauth/services/migrations/0003_delete_groupcache.py
new file mode 100644
index 00000000..cc949a31
--- /dev/null
+++ b/allianceauth/services/migrations/0003_delete_groupcache.py
@@ -0,0 +1,18 @@
+# -*- 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',
+ ),
+ ]
diff --git a/allianceauth/services/migrations/__init__.py b/allianceauth/services/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/allianceauth/services/models.py b/allianceauth/services/models.py
index e69de29b..01e60f8e 100644
--- a/allianceauth/services/models.py
+++ b/allianceauth/services/models.py
@@ -0,0 +1,17 @@
+from django.db import models
+
+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 '
+ ''
+ 'name format documentation')
+ states = models.ManyToManyField(State, help_text="States to apply this format to. You should only have one "
+ "formatter for each state for each service.")
+
+
diff --git a/allianceauth/services/modules/discord/auth_hooks.py b/allianceauth/services/modules/discord/auth_hooks.py
index 3de5597d..57caa83e 100644
--- a/allianceauth/services/modules/discord/auth_hooks.py
+++ b/allianceauth/services/modules/discord/auth_hooks.py
@@ -18,6 +18,7 @@ class DiscordService(ServicesHook):
self.name = 'discord'
self.service_ctrl_template = 'services/discord/discord_service_ctrl.html'
self.access_perm = 'discord.access_discord'
+ self.name_format = '{character_name}'
def delete_user(self, user, notify_user=False):
logger.debug('Deleting user %s %s account' % (user, self.name))
diff --git a/allianceauth/services/modules/discord/tasks.py b/allianceauth/services/modules/discord/tasks.py
index c53d048c..359831b1 100644
--- a/allianceauth/services/modules/discord/tasks.py
+++ b/allianceauth/services/modules/discord/tasks.py
@@ -6,6 +6,7 @@ from django.core.exceptions import ObjectDoesNotExist
from allianceauth.notifications import notify
from allianceauth.celery import app
+from allianceauth.services.hooks import NameFormatter
from .manager import DiscordOAuthManager, DiscordApiBackoff
from .models import DiscordUser
@@ -102,7 +103,7 @@ class DiscordTasks:
character = user.profile.main_character
logger.debug("Updating user %s discord nickname to %s" % (user, character.character_name))
try:
- DiscordOAuthManager.update_nickname(user.discord.uid, character.character_name)
+ DiscordOAuthManager.update_nickname(user.discord.uid, DiscordTasks.get_nickname(user))
except Exception as e:
if self:
logger.exception("Discord nickname sync failed for %s, retrying in 10 mins" % user)
@@ -126,3 +127,8 @@ class DiscordTasks:
@classmethod
def disable(cls):
DiscordUser.objects.all().delete()
+
+ @staticmethod
+ def get_nickname(user):
+ from .auth_hooks import DiscordService
+ return NameFormatter(DiscordService(), user).format_name()
diff --git a/allianceauth/services/modules/discourse/auth_hooks.py b/allianceauth/services/modules/discourse/auth_hooks.py
index f14f57ab..514e0c98 100644
--- a/allianceauth/services/modules/discourse/auth_hooks.py
+++ b/allianceauth/services/modules/discourse/auth_hooks.py
@@ -18,6 +18,7 @@ class DiscourseService(ServicesHook):
self.name = 'discourse'
self.service_ctrl_template = 'services/discourse/discourse_service_ctrl.html'
self.access_perm = 'discourse.access_discourse'
+ self.name_format = '{character_name}'
def delete_user(self, user, notify_user=False):
logger.debug('Deleting user %s %s account' % (user, self.name))
diff --git a/allianceauth/services/modules/discourse/tasks.py b/allianceauth/services/modules/discourse/tasks.py
index 898dc27c..71bca870 100644
--- a/allianceauth/services/modules/discourse/tasks.py
+++ b/allianceauth/services/modules/discourse/tasks.py
@@ -5,6 +5,7 @@ from django.core.exceptions import ObjectDoesNotExist
from allianceauth.celery import app
from allianceauth.notifications import notify
+from allianceauth.services.hooks import NameFormatter
from .manager import DiscourseManager
from .models import DiscourseUser
@@ -56,3 +57,8 @@ class DiscourseTasks:
logger.debug("Updating ALL discourse groups")
for discourse_user in DiscourseUser.objects.filter(enabled=True):
DiscourseTasks.update_groups.delay(discourse_user.user.pk)
+
+ @staticmethod
+ def get_username(user):
+ from .auth_hooks import DiscourseService
+ return NameFormatter(DiscourseService(), user).format_name()
diff --git a/allianceauth/services/modules/discourse/views.py b/allianceauth/services/modules/discourse/views.py
index 594c5bbf..270ce9c0 100644
--- a/allianceauth/services/modules/discourse/views.py
+++ b/allianceauth/services/modules/discourse/views.py
@@ -71,7 +71,7 @@ def discourse_sso(request):
## Build the return payload
- username = DiscourseManager._sanitize_username(main_char.character_name)
+ username = DiscourseManager._sanitize_username(DiscourseTasks.get_username(request.user))
qs = parse_qs(decoded)
params = {
diff --git a/allianceauth/services/modules/example/auth_hooks.py b/allianceauth/services/modules/example/auth_hooks.py
index d81d9075..5b1fe7b0 100644
--- a/allianceauth/services/modules/example/auth_hooks.py
+++ b/allianceauth/services/modules/example/auth_hooks.py
@@ -10,6 +10,7 @@ class ExampleService(ServicesHook):
ServicesHook.__init__(self)
self.urlpatterns = urlpatterns
self.service_url = 'http://exampleservice.example.com'
+ self.name = 'example'
"""
Overload base methods here to implement functionality
diff --git a/allianceauth/services/modules/ips4/auth_hooks.py b/allianceauth/services/modules/ips4/auth_hooks.py
index 45f5c511..83a9855b 100644
--- a/allianceauth/services/modules/ips4/auth_hooks.py
+++ b/allianceauth/services/modules/ips4/auth_hooks.py
@@ -14,6 +14,7 @@ class Ips4Service(ServicesHook):
self.urlpatterns = urlpatterns
self.service_url = settings.IPS4_URL
self.access_perm = 'ips4.access_ips4'
+ self.name_format = '{character_name}'
@property
def title(self):
diff --git a/allianceauth/services/modules/ips4/tasks.py b/allianceauth/services/modules/ips4/tasks.py
index e452e0be..b6c5dce8 100644
--- a/allianceauth/services/modules/ips4/tasks.py
+++ b/allianceauth/services/modules/ips4/tasks.py
@@ -1,6 +1,7 @@
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
+from allianceauth.services.hooks import NameFormatter
from .manager import Ips4Manager
from .models import Ips4User
@@ -33,3 +34,8 @@ class Ips4Tasks:
def disable():
logging.debug("Deleting all IPS4 users")
Ips4User.objects.all().delete()
+
+ @staticmethod
+ def get_username(user):
+ from .auth_hooks import Ips4Service
+ return NameFormatter(Ips4Service(), user).format_name()
diff --git a/allianceauth/services/modules/ips4/views.py b/allianceauth/services/modules/ips4/views.py
index a86de328..8c77c726 100644
--- a/allianceauth/services/modules/ips4/views.py
+++ b/allianceauth/services/modules/ips4/views.py
@@ -20,7 +20,7 @@ def activate_ips4(request):
logger.debug("activate_ips4 called by user %s" % request.user)
character = request.user.profile.main_character
logger.debug("Adding IPS4 user for user %s with main character %s" % (request.user, character))
- result = Ips4Manager.add_user(character.character_name, request.user.email)
+ result = Ips4Manager.add_user(Ips4Tasks.get_username(request.user), request.user.email)
# if empty we failed
if result[0] != "" and not Ips4Tasks.has_account(request.user):
ips_user = Ips4User.objects.create(user=request.user, id=result[2], username=result[0])
diff --git a/allianceauth/services/modules/mumble/auth_hooks.py b/allianceauth/services/modules/mumble/auth_hooks.py
index 94301aaf..2ad1e229 100644
--- a/allianceauth/services/modules/mumble/auth_hooks.py
+++ b/allianceauth/services/modules/mumble/auth_hooks.py
@@ -21,6 +21,7 @@ class MumbleService(ServicesHook):
self.service_url = settings.MUMBLE_URL
self.access_perm = 'mumble.access_mumble'
self.service_ctrl_template = 'services/mumble/mumble_service_ctrl.html'
+ self.name_format = '[{corp_ticker}]{character_name}'
def delete_user(self, user, notify_user=False):
logging.debug("Deleting user %s %s account" % (user, self.name))
diff --git a/allianceauth/services/modules/mumble/manager.py b/allianceauth/services/modules/mumble/manager.py
index faab95f5..0ac2f5b3 100755
--- a/allianceauth/services/modules/mumble/manager.py
+++ b/allianceauth/services/modules/mumble/manager.py
@@ -26,25 +26,14 @@ class MumbleManager:
def __generate_random_pass():
return ''.join([random.choice(string.ascii_letters + string.digits) for n in range(16)])
- @staticmethod
- def __generate_username(username, corp_ticker):
- return "[" + corp_ticker + "]" + username
-
- @staticmethod
- def __generate_username_blue(username, corp_ticker):
- return "[BLUE][" + corp_ticker + "]" + username
-
@classmethod
def _gen_pwhash(cls, password):
return bcrypt_sha256.encrypt(password.encode('utf-8'))
@classmethod
- def create_user(cls, user, corp_ticker, username, blue=False):
- logger.debug("Creating%s mumble user with username %s and ticker %s" % (' blue' if blue else '',
- username, corp_ticker))
- username_clean = cls.__santatize_username(
- cls.__generate_username_blue(username, corp_ticker) if blue else
- cls.__generate_username(username, corp_ticker))
+ def create_user(cls, user, username):
+ logger.debug("Creating mumble user with username %s" % (username))
+ username_clean = cls.__santatize_username(username)
password = cls.__generate_random_pass()
pwhash = cls._gen_pwhash(password)
logger.debug("Proceeding with mumble user creation: clean username %s, pwhash starts with %s" % (
diff --git a/allianceauth/services/modules/mumble/tasks.py b/allianceauth/services/modules/mumble/tasks.py
index 9fc5c65b..85be0a69 100644
--- a/allianceauth/services/modules/mumble/tasks.py
+++ b/allianceauth/services/modules/mumble/tasks.py
@@ -2,6 +2,7 @@ import logging
from django.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist
+from allianceauth.services.hooks import NameFormatter
from allianceauth.celery import app
from .manager import MumbleManager
@@ -26,6 +27,11 @@ class MumbleTasks:
logger.info("Deleting all MumbleUser models")
MumbleUser.objects.all().delete()
+ @staticmethod
+ def get_username(user):
+ from .auth_hooks import MumbleService
+ return NameFormatter(MumbleService(), user).format_name()
+
@staticmethod
@app.task(bind=True, name="mumble.update_groups")
def update_groups(self, pk):
diff --git a/allianceauth/services/modules/mumble/views.py b/allianceauth/services/modules/mumble/views.py
index 6df48c5a..815696b5 100644
--- a/allianceauth/services/modules/mumble/views.py
+++ b/allianceauth/services/modules/mumble/views.py
@@ -18,10 +18,9 @@ ACCESS_PERM = 'mumble.access_mumble'
def activate_mumble(request):
logger.debug("activate_mumble called by user %s" % request.user)
character = request.user.profile.main_character
- ticker = character.corporation_ticker
logger.debug("Adding mumble user for %s with main character %s" % (request.user, character))
- result = MumbleManager.create_user(request.user, ticker, character.character_name)
+ result = MumbleManager.create_user(request.user, MumbleTasks.get_username(request.user))
if result:
logger.debug("Updated authserviceinfo for user %s with mumble credentials. Updating groups." % request.user)
diff --git a/allianceauth/services/modules/openfire/auth_hooks.py b/allianceauth/services/modules/openfire/auth_hooks.py
index 63335755..5a75d6df 100644
--- a/allianceauth/services/modules/openfire/auth_hooks.py
+++ b/allianceauth/services/modules/openfire/auth_hooks.py
@@ -18,6 +18,7 @@ class OpenfireService(ServicesHook):
self.urlpatterns = urlpatterns
self.service_url = settings.JABBER_URL
self.access_perm = 'openfire.access_openfire'
+ self.name_format = '{character_name}'
@property
def title(self):
diff --git a/allianceauth/services/modules/openfire/tasks.py b/allianceauth/services/modules/openfire/tasks.py
index b39c9e97..bec8371d 100644
--- a/allianceauth/services/modules/openfire/tasks.py
+++ b/allianceauth/services/modules/openfire/tasks.py
@@ -6,6 +6,7 @@ from allianceauth.notifications import notify
from allianceauth.celery import app
from allianceauth.services.modules.openfire.manager import OpenfireManager
+from allianceauth.services.hooks import NameFormatter
from .models import OpenfireUser
logger = logging.getLogger(__name__)
@@ -65,3 +66,8 @@ class OpenfireTasks:
logger.debug("Updating ALL jabber groups")
for openfire_user in OpenfireUser.objects.exclude(username__exact=''):
OpenfireTasks.update_groups.delay(openfire_user.user.pk)
+
+ @staticmethod
+ def get_username(user):
+ from .auth_hooks import OpenfireService
+ return NameFormatter(OpenfireService(), user).format_name()
diff --git a/allianceauth/services/modules/openfire/views.py b/allianceauth/services/modules/openfire/views.py
index e3598718..bc38f0fa 100644
--- a/allianceauth/services/modules/openfire/views.py
+++ b/allianceauth/services/modules/openfire/views.py
@@ -23,7 +23,7 @@ def activate_jabber(request):
logger.debug("activate_jabber called by user %s" % request.user)
character = request.user.profile.main_character
logger.debug("Adding jabber user for user %s with main character %s" % (request.user, character))
- info = OpenfireManager.add_user(character.character_name)
+ info = OpenfireManager.add_user(OpenfireTasks.get_username(request.user))
# If our username is blank means we already had a user
if info[0] is not "":
OpenfireUser.objects.update_or_create(user=request.user, defaults={'username': info[0]})
diff --git a/allianceauth/services/modules/phpbb3/auth_hooks.py b/allianceauth/services/modules/phpbb3/auth_hooks.py
index 8f04de98..3f09c7d4 100644
--- a/allianceauth/services/modules/phpbb3/auth_hooks.py
+++ b/allianceauth/services/modules/phpbb3/auth_hooks.py
@@ -18,6 +18,7 @@ class Phpbb3Service(ServicesHook):
self.urlpatterns = urlpatterns
self.service_url = settings.PHPBB3_URL
self.access_perm = 'phpbb3.access_phpbb3'
+ self.name_format = '{character_name}'
@property
def title(self):
diff --git a/allianceauth/services/modules/phpbb3/tasks.py b/allianceauth/services/modules/phpbb3/tasks.py
index b0d963f2..10cd1911 100644
--- a/allianceauth/services/modules/phpbb3/tasks.py
+++ b/allianceauth/services/modules/phpbb3/tasks.py
@@ -5,6 +5,7 @@ from django.core.exceptions import ObjectDoesNotExist
from allianceauth.celery import app
from allianceauth.notifications import notify
+from allianceauth.services.hooks import NameFormatter
from .manager import Phpbb3Manager
from .models import Phpbb3User
@@ -64,3 +65,8 @@ class Phpbb3Tasks:
@staticmethod
def disable():
Phpbb3User.objects.all().delete()
+
+ @staticmethod
+ def get_username(user):
+ from .auth_hooks import Phpbb3Service
+ return NameFormatter(Phpbb3Service(), user).format_name()
diff --git a/allianceauth/services/modules/phpbb3/views.py b/allianceauth/services/modules/phpbb3/views.py
index e8dcf16d..eb7f8460 100644
--- a/allianceauth/services/modules/phpbb3/views.py
+++ b/allianceauth/services/modules/phpbb3/views.py
@@ -21,7 +21,7 @@ def activate_forum(request):
# Valid now we get the main characters
character = request.user.profile.main_character
logger.debug("Adding phpbb user for user %s with main character %s" % (request.user, character))
- result = Phpbb3Manager.add_user(character.character_name, request.user.email, ['REGISTERED'],
+ result = Phpbb3Manager.add_user(Phpbb3Tasks.get_username(request.user), request.user.email, ['REGISTERED'],
character.character_id)
# if empty we failed
if result[0] != "":
diff --git a/allianceauth/services/modules/seat/auth_hooks.py b/allianceauth/services/modules/seat/auth_hooks.py
index 6b51089b..bad00873 100644
--- a/allianceauth/services/modules/seat/auth_hooks.py
+++ b/allianceauth/services/modules/seat/auth_hooks.py
@@ -18,6 +18,7 @@ class SeatService(ServicesHook):
self.name = 'seat'
self.service_url = settings.SEAT_URL
self.access_perm = 'seat.access_seat'
+ self.name_format = '{character_name}'
@property
def title(self):
diff --git a/allianceauth/services/modules/seat/tasks.py b/allianceauth/services/modules/seat/tasks.py
index 185e2015..29515b63 100644
--- a/allianceauth/services/modules/seat/tasks.py
+++ b/allianceauth/services/modules/seat/tasks.py
@@ -5,6 +5,7 @@ from django.core.exceptions import ObjectDoesNotExist
from allianceauth.celery import app
from allianceauth.notifications import notify
+from allianceauth.services.hooks import NameFormatter
from .manager import SeatManager
from .models import SeatUser
@@ -64,3 +65,8 @@ class SeatTasks:
@staticmethod
def deactivate():
SeatUser.objects.all().delete()
+
+ @staticmethod
+ def get_username(user):
+ from .auth_hooks import SeatService
+ return NameFormatter(SeatService(), user).format_name()
diff --git a/allianceauth/services/modules/seat/views.py b/allianceauth/services/modules/seat/views.py
index 157cc335..b30e3bc9 100644
--- a/allianceauth/services/modules/seat/views.py
+++ b/allianceauth/services/modules/seat/views.py
@@ -25,7 +25,7 @@ def activate_seat(request):
stat = SeatManager.check_user_status(character.character_name)
if stat == {}:
logger.debug("User not found, adding SeAT user for user %s with main character %s" % (request.user, character))
- result = SeatManager.add_user(character.character_name, request.user.email)
+ result = SeatManager.add_user(SeatTasks.get_username(request.user), request.user.email)
else:
logger.debug("User found, resetting password")
username = SeatManager.enable_user(stat["name"])
diff --git a/allianceauth/services/modules/smf/auth_hooks.py b/allianceauth/services/modules/smf/auth_hooks.py
index d6ccf5d5..9baa3136 100644
--- a/allianceauth/services/modules/smf/auth_hooks.py
+++ b/allianceauth/services/modules/smf/auth_hooks.py
@@ -18,6 +18,7 @@ class SmfService(ServicesHook):
self.urlpatterns = urlpatterns
self.service_url = settings.SMF_URL
self.access_perm = 'smf.access_smf'
+ self.name_format = '{character_name}'
@property
def title(self):
diff --git a/allianceauth/services/modules/smf/tasks.py b/allianceauth/services/modules/smf/tasks.py
index be445e3a..f00e1a5d 100644
--- a/allianceauth/services/modules/smf/tasks.py
+++ b/allianceauth/services/modules/smf/tasks.py
@@ -5,6 +5,7 @@ from django.core.exceptions import ObjectDoesNotExist
from allianceauth.celery import app
from allianceauth.notifications import notify
+from allianceauth.services.hooks import NameFormatter
from .manager import SmfManager
from .models import SmfUser
@@ -64,3 +65,8 @@ class SmfTasks:
logger.debug("Updating ALL smf groups")
for user in SmfUser.objects.exclude(username__exact=''):
SmfTasks.update_groups.delay(user.user_id)
+
+ @staticmethod
+ def get_username(user):
+ from .auth_hooks import SmfService
+ return NameFormatter(SmfService(), user).format_name()
diff --git a/allianceauth/services/modules/smf/views.py b/allianceauth/services/modules/smf/views.py
index 8fc1e112..2d23940b 100644
--- a/allianceauth/services/modules/smf/views.py
+++ b/allianceauth/services/modules/smf/views.py
@@ -21,7 +21,8 @@ def activate_smf(request):
# Valid now we get the main characters
character = request.user.profile.main_character
logger.debug("Adding smf user for user %s with main character %s" % (request.user, character))
- result = SmfManager.add_user(character.character_name, request.user.email, ['Member'], character.character_id)
+ result = SmfManager.add_user(SmfTasks.get_username(request.user), request.user.email, ['Member'],
+ character.character_id)
# if empty we failed
if result[0] != "":
SmfUser.objects.update_or_create(user=request.user, defaults={'username': result[0]})
diff --git a/allianceauth/services/modules/teamspeak3/auth_hooks.py b/allianceauth/services/modules/teamspeak3/auth_hooks.py
index 1e481de5..f4ef6af1 100644
--- a/allianceauth/services/modules/teamspeak3/auth_hooks.py
+++ b/allianceauth/services/modules/teamspeak3/auth_hooks.py
@@ -18,6 +18,7 @@ class Teamspeak3Service(ServicesHook):
self.urlpatterns = urlpatterns
self.service_ctrl_template = 'services/teamspeak3/teamspeak3_service_ctrl.html'
self.access_perm = 'teamspeak3.access_teamspeak3'
+ self.name_format = '[{corp_ticker}]{character_name}'
def delete_user(self, user, notify_user=False):
logger.debug('Deleting user %s %s account' % (user, self.name))
diff --git a/allianceauth/services/modules/teamspeak3/manager.py b/allianceauth/services/modules/teamspeak3/manager.py
index a0da8587..c88dc22c 100755
--- a/allianceauth/services/modules/teamspeak3/manager.py
+++ b/allianceauth/services/modules/teamspeak3/manager.py
@@ -50,11 +50,6 @@ class Teamspeak3Manager:
sanatized = sanatized.replace("'", "-")
return sanatized
- @staticmethod
- def __generate_username(username, corp_ticker):
- sanatized = "[" + corp_ticker + "]" + username
- return sanatized[:30]
-
def _get_userid(self, uid):
logger.debug("Looking for uid %s on TS3 server." % uid)
try:
@@ -184,8 +179,8 @@ class Teamspeak3Manager:
except:
logger.exception("An unhandled exception has occured while syncing TS groups.")
- def add_user(self, username, corp_ticker):
- username_clean = self.__santatize_username(self.__generate_username(username, corp_ticker))
+ def add_user(self, username):
+ username_clean = self.__santatize_username(username[:30])
logger.debug("Adding user to TS3 server with cleaned username %s" % username_clean)
server_groups = self._group_list()
diff --git a/allianceauth/services/modules/teamspeak3/tasks.py b/allianceauth/services/modules/teamspeak3/tasks.py
index 7a0c2fab..1accc78a 100644
--- a/allianceauth/services/modules/teamspeak3/tasks.py
+++ b/allianceauth/services/modules/teamspeak3/tasks.py
@@ -5,6 +5,7 @@ from django.core.exceptions import ObjectDoesNotExist
from allianceauth.celery import app
from allianceauth.notifications import notify
+from allianceauth.services.hooks import NameFormatter
from .manager import Teamspeak3Manager
from .models import AuthTS, TSgroup, UserTSgroup, Teamspeak3User
from .util.ts3 import TeamspeakError
@@ -85,3 +86,8 @@ class Teamspeak3Tasks:
logger.debug("Updating ALL teamspeak3 groups")
for user in Teamspeak3User.objects.exclude(uid__exact=''):
Teamspeak3Tasks.update_groups.delay(user.user_id)
+
+ @staticmethod
+ def get_username(user):
+ from .auth_hooks import Teamspeak3Service
+ return NameFormatter(Teamspeak3Service(), user).format_name()
diff --git a/allianceauth/services/modules/teamspeak3/views.py b/allianceauth/services/modules/teamspeak3/views.py
index 4c0d06bf..31de22e3 100644
--- a/allianceauth/services/modules/teamspeak3/views.py
+++ b/allianceauth/services/modules/teamspeak3/views.py
@@ -23,7 +23,7 @@ def activate_teamspeak3(request):
ticker = character.corporation_ticker
with Teamspeak3Manager() as ts3man:
logger.debug("Adding TS3 user for user %s with main character %s" % (request.user, character))
- result = ts3man.add_user(character.character_name, ticker)
+ result = ts3man.add_user(Teamspeak3Tasks.get_username(request.user))
# if its empty we failed
if result[0] is not "":
diff --git a/allianceauth/services/modules/xenforo/auth_hooks.py b/allianceauth/services/modules/xenforo/auth_hooks.py
index 89416eaf..3f5d149f 100644
--- a/allianceauth/services/modules/xenforo/auth_hooks.py
+++ b/allianceauth/services/modules/xenforo/auth_hooks.py
@@ -16,6 +16,7 @@ class XenforoService(ServicesHook):
self.name = 'xenforo'
self.urlpatterns = urlpatterns
self.access_perm = 'xenforo.access_xenforo'
+ self.name_format = '{character_name}'
@property
def title(self):
diff --git a/allianceauth/services/modules/xenforo/tasks.py b/allianceauth/services/modules/xenforo/tasks.py
index 5768a2db..0cb06694 100644
--- a/allianceauth/services/modules/xenforo/tasks.py
+++ b/allianceauth/services/modules/xenforo/tasks.py
@@ -3,6 +3,7 @@ import logging
from django.core.exceptions import ObjectDoesNotExist
from allianceauth.notifications import notify
+from allianceauth.services.hooks import NameFormatter
from .manager import XenForoManager
from .models import XenforoUser
@@ -35,3 +36,8 @@ class XenforoTasks:
def disable(cls):
logger.debug("Deleting ALL XenForo users")
XenforoUser.objects.all().delete()
+
+ @staticmethod
+ def get_username(user):
+ from .auth_hooks import XenforoService
+ return NameFormatter(XenforoService(), user).format_name()
diff --git a/allianceauth/services/modules/xenforo/views.py b/allianceauth/services/modules/xenforo/views.py
index c84cb280..3d037350 100644
--- a/allianceauth/services/modules/xenforo/views.py
+++ b/allianceauth/services/modules/xenforo/views.py
@@ -20,7 +20,7 @@ def activate_xenforo_forum(request):
logger.debug("activate_xenforo_forum called by user %s" % request.user)
character = request.user.profile.main_character
logger.debug("Adding XenForo user for user %s with main character %s" % (request.user, character))
- result = XenForoManager.add_user(character.character_name, request.user.email)
+ result = XenForoManager.add_user(XenforoTasks.get_username(request.user), request.user.email)
# Based on XenAPI's response codes
if result['response']['status_code'] == 200:
XenforoUser.objects.update_or_create(user=request.user, defaults={'username': result['username']})
diff --git a/allianceauth/services/tests/test_nameformatter.py b/allianceauth/services/tests/test_nameformatter.py
new file mode 100644
index 00000000..79f328c1
--- /dev/null
+++ b/allianceauth/services/tests/test_nameformatter.py
@@ -0,0 +1,103 @@
+from django.test import TestCase
+from allianceauth.tests.auth_utils import AuthUtils
+from allianceauth.eveonline.models import EveAllianceInfo, EveCorporationInfo, EveCharacter
+from ..models import NameFormatConfig
+from ..hooks import NameFormatter
+from ..modules.example.auth_hooks import ExampleService
+
+
+class NameFormatterTestCase(TestCase):
+ def setUp(self):
+ self.member = AuthUtils.create_user('auth_member', disconnect_signals=True)
+
+ self.alliance = EveAllianceInfo.objects.create(
+ alliance_id='3456',
+ alliance_name='alliance name',
+ alliance_ticker='TIKR',
+ executor_corp_id='2345',
+ )
+
+ self.corp = EveCorporationInfo.objects.create(
+ corporation_id='2345',
+ corporation_name='corp name',
+ corporation_ticker='TIKK',
+ member_count=10,
+ alliance=self.alliance,
+ )
+
+ self.char = EveCharacter.objects.create(
+ character_id='1234',
+ character_name='test character',
+ corporation_id='2345',
+ corporation_name='test corp',
+ corporation_ticker='TIKK',
+ alliance_id='3456',
+ alliance_name='alliance name',
+ )
+ self.member.profile.main_character = self.char
+ self.member.profile.save()
+
+ def test_formatter_prop(self):
+ config = NameFormatConfig.objects.create(
+ service_name='example',
+ default_to_username=False,
+ format='{character_name}',
+ )
+
+ config.states.add(self.member.profile.state)
+
+ formatter = NameFormatter(ExampleService(), self.member)
+
+ self.assertEqual(config, formatter.formatter_config)
+
+ def test_default_formatter(self):
+
+ formatter = NameFormatter(ExampleService(), self.member)
+
+ self.assertEqual(NameFormatter.DEFAULT_FORMAT, formatter.default_formatter)
+ # Test the default is returned when the service has no default
+ self.assertEqual(NameFormatter.DEFAULT_FORMAT, formatter.string_formatter)
+
+ def test_get_format_data(self):
+ config = NameFormatConfig.objects.create(
+ service_name='example',
+ default_to_username=False,
+ format='{alliance_ticker}', # Ensures that alliance_ticker is filled
+ )
+ config.states.add(self.member.profile.state)
+
+ formatter = NameFormatter(ExampleService(), self.member)
+
+ result = formatter.get_format_data()
+
+ self.assertIn('character_name', result)
+ self.assertEqual(result['character_name'], self.char.character_name)
+ self.assertIn('character_id', result)
+ self.assertEqual(result['character_id'], self.char.character_id)
+ self.assertIn('corp_name', result)
+ self.assertEqual(result['corp_name'], self.char.corporation_name)
+ self.assertIn('corp_id', result)
+ self.assertEqual(result['corp_id'], self.char.corporation_id)
+ self.assertIn('corp_ticker', result)
+ self.assertEqual(result['corp_ticker'], self.char.corporation_ticker)
+ self.assertIn('alliance_name', result)
+ self.assertEqual(result['alliance_name'], self.char.alliance_name)
+ self.assertIn('alliance_ticker', result)
+ self.assertEqual(result['alliance_ticker'], self.char.alliance.alliance_ticker)
+ self.assertIn('alliance_id', result)
+ self.assertEqual(result['alliance_id'], self.char.alliance_id)
+ self.assertIn('username', result)
+ self.assertEqual(result['username'], self.member.username)
+
+ def test_format_name(self):
+ config = NameFormatConfig.objects.create(
+ service_name='example',
+ default_to_username=False,
+ format='{character_id} test {username}',
+ )
+ config.states.add(self.member.profile.state)
+ formatter = NameFormatter(ExampleService(), self.member)
+
+ result = formatter.format_name()
+
+ self.assertEqual('1234 test auth_member', result)
diff --git a/docs/features/index.md b/docs/features/index.md
index dd317ca0..55878306 100644
--- a/docs/features/index.md
+++ b/docs/features/index.md
@@ -9,4 +9,5 @@
corpstats
groups
permissions_tool
+ nameformats
```
diff --git a/docs/features/nameformats.md b/docs/features/nameformats.md
new file mode 100644
index 00000000..707f710a
--- /dev/null
+++ b/docs/features/nameformats.md
@@ -0,0 +1,84 @@
+# Services Name Formats
+
+```eval_rst
+.. note::
+ New in 2.0
+```
+
+Each service's username or nickname, depending on which the service supports, can be customised through the use of the Name Formatter Config provided the service supports custom formats. This config can be found in the admin panel under **Services -> Name format config**
+
+Currently the following services support custom name formats:
+
+```eval_rst
++-------------+-----------+-------------------------------------+
+| Service | Used with | Default Formatter |
++=============+===========+=====================================+
+| Discord | Nickname | ``{character_name}`` |
++-------------+-----------+-------------------------------------+
+| Discourse | Username | ``{character_name}`` |
++-------------+-----------+-------------------------------------+
+| IPS4 | Username | ``{character_name}`` |
++-------------+-----------+-------------------------------------+
+| Mumble | Username | ``[{corp_ticker}]{character_name}`` |
++-------------+-----------+-------------------------------------+
+| Openfire | Username | ``{character_name}`` |
++-------------+-----------+-------------------------------------+
+| phpBB3 | Username | ``{character_name}`` |
++-------------+-----------+-------------------------------------+
+| SeAT | Username | ``{character_name}`` |
++-------------+-----------+-------------------------------------+
+| SMF | Username | ``{character_name}`` |
++-------------+-----------+-------------------------------------+
+| Teamspeak 3 | Nickname | ``[{corp_ticker}]{character_name}`` |
++-------------+-----------+-------------------------------------+
+| Xenforo | Username | ``{character_name}`` |
++-------------+-----------+-------------------------------------+
+```
+
+```eval_rst
+.. note::
+ It's important to note here, before we get into what you can do with a name formatter, that before the generated name is passed off to the service to create an account it will be sanitised to remove characters (the letters and numbers etc) that the service cannot support. This means that, despite what you configured, the service may display something different. It is up to you to test your formatter and understand how your format may be disrupted by a certain services sanitisation function.
+```
+
+## Available format data
+
+The following fields are available from a users account and main character:
+
+ - `username` - Alliance Auth username
+ - `character_id`
+ - `character_name`
+ - `corp_id`
+ - `corp_name`
+ - `corp_ticker`
+ - `alliance_id`
+ - `alliance_name`
+ - `alliance_ticker`
+
+## Building a formatter string
+
+The name formatter uses the advanced string formatting specified by [PEP-3101](https://www.python.org/dev/peps/pep-3101/). Anything supported by this specification is supported in a name formatter.
+
+A more digestable documentation of string formatting in Python is available on the [PyFormat](https://pyformat.info/) website.
+
+Some examples of strings you could use:
+```eval_rst
++------------------------------------------+---------------------------+
+| Formatter | Result |
++==========================================+===========================+
+| ``{alliance_ticker} - {character_name}`` | ``MYALLI - My Character`` |
++------------------------------------------+---------------------------+
+| ``[{corp_ticker}] {character_name}`` | ``[CORP] My Character`` |
++------------------------------------------+---------------------------+
+| ``{{{corp_name}}}{character_name}`` | ``{My Corp}My Character`` |
++------------------------------------------+---------------------------+
+```
+
+```eval_rst
+.. important::
+ For most services, name formats only take effect when a user creates an account. This means if you create or update a name formatter it wont retroactively alter the format of users names. There are some exceptions to this where the service updates nicknames on a periodic basis. Check the service's documentation to see which of these apply.
+```
+
+```eval_rst
+.. important::
+ You must only create one formatter per service per state. E.g. don't create two formatters for Mumble for the Member state. In this case one of the formatters will be used and it may not be the formatter you are expecting.
+```