Name generator/formatter (#897)

* Squash services migrations

* Add name to example service to allow it to be used in tests

* Add name formatter to services

* Add documentation
This commit is contained in:
Basraah 2017-10-11 12:34:31 +10:00 committed by GitHub
parent b95bb9aa6a
commit ef37cb3ea5
45 changed files with 518 additions and 33 deletions

View File

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

View File

@ -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)

View File

@ -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)

View File

@ -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)),
],
),
]

View File

@ -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 = [
]

View File

@ -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',
),
]

View File

@ -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 <a href="https://allianceauth.readthedocs.io/en/latest/features/nameformats">name format documentation</a>', 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')),
],
),
]

View File

@ -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',
),
]

View File

@ -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 '
'<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.")

View File

@ -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))

View File

@ -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()

View File

@ -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))

View File

@ -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()

View File

@ -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 = {

View File

@ -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

View File

@ -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):

View File

@ -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()

View File

@ -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])

View File

@ -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))

View File

@ -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" % (

View File

@ -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):

View File

@ -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)

View File

@ -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):

View File

@ -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()

View File

@ -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]})

View File

@ -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):

View File

@ -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()

View File

@ -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] != "":

View File

@ -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):

View File

@ -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()

View File

@ -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"])

View File

@ -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):

View File

@ -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()

View File

@ -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]})

View File

@ -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))

View File

@ -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()

View File

@ -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()

View File

@ -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 "":

View File

@ -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):

View File

@ -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()

View File

@ -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']})

View File

@ -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)

View File

@ -9,4 +9,5 @@
corpstats
groups
permissions_tool
nameformats
```

View File

@ -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.
```