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.conf.urls import include, url
from django.template.loader import render_to_string 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 allianceauth.hooks import get_hooks
from .models import NameFormatConfig
class ServicesHook: class ServicesHook:
""" """
@ -122,3 +128,77 @@ class MenuItemHook:
class UrlHook: class UrlHook:
def __init__(self, urls, namespace, base_url): def __init__(self, urls, namespace, base_url):
self.include_pattern = url(base_url, include(urls, namespace=namespace)) 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.name = 'discord'
self.service_ctrl_template = 'services/discord/discord_service_ctrl.html' self.service_ctrl_template = 'services/discord/discord_service_ctrl.html'
self.access_perm = 'discord.access_discord' self.access_perm = 'discord.access_discord'
self.name_format = '{character_name}'
def delete_user(self, user, notify_user=False): def delete_user(self, user, notify_user=False):
logger.debug('Deleting user %s %s account' % (user, self.name)) 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.notifications import notify
from allianceauth.celery import app from allianceauth.celery import app
from allianceauth.services.hooks import NameFormatter
from .manager import DiscordOAuthManager, DiscordApiBackoff from .manager import DiscordOAuthManager, DiscordApiBackoff
from .models import DiscordUser from .models import DiscordUser
@ -102,7 +103,7 @@ class DiscordTasks:
character = user.profile.main_character character = user.profile.main_character
logger.debug("Updating user %s discord nickname to %s" % (user, character.character_name)) logger.debug("Updating user %s discord nickname to %s" % (user, character.character_name))
try: try:
DiscordOAuthManager.update_nickname(user.discord.uid, character.character_name) DiscordOAuthManager.update_nickname(user.discord.uid, DiscordTasks.get_nickname(user))
except Exception as e: except Exception as e:
if self: if self:
logger.exception("Discord nickname sync failed for %s, retrying in 10 mins" % user) logger.exception("Discord nickname sync failed for %s, retrying in 10 mins" % user)
@ -126,3 +127,8 @@ class DiscordTasks:
@classmethod @classmethod
def disable(cls): def disable(cls):
DiscordUser.objects.all().delete() 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.name = 'discourse'
self.service_ctrl_template = 'services/discourse/discourse_service_ctrl.html' self.service_ctrl_template = 'services/discourse/discourse_service_ctrl.html'
self.access_perm = 'discourse.access_discourse' self.access_perm = 'discourse.access_discourse'
self.name_format = '{character_name}'
def delete_user(self, user, notify_user=False): def delete_user(self, user, notify_user=False):
logger.debug('Deleting user %s %s account' % (user, self.name)) 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.celery import app
from allianceauth.notifications import notify from allianceauth.notifications import notify
from allianceauth.services.hooks import NameFormatter
from .manager import DiscourseManager from .manager import DiscourseManager
from .models import DiscourseUser from .models import DiscourseUser
@ -56,3 +57,8 @@ class DiscourseTasks:
logger.debug("Updating ALL discourse groups") logger.debug("Updating ALL discourse groups")
for discourse_user in DiscourseUser.objects.filter(enabled=True): for discourse_user in DiscourseUser.objects.filter(enabled=True):
DiscourseTasks.update_groups.delay(discourse_user.user.pk) 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 ## 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) qs = parse_qs(decoded)
params = { params = {

View File

@ -10,6 +10,7 @@ class ExampleService(ServicesHook):
ServicesHook.__init__(self) ServicesHook.__init__(self)
self.urlpatterns = urlpatterns self.urlpatterns = urlpatterns
self.service_url = 'http://exampleservice.example.com' self.service_url = 'http://exampleservice.example.com'
self.name = 'example'
""" """
Overload base methods here to implement functionality Overload base methods here to implement functionality

View File

@ -14,6 +14,7 @@ class Ips4Service(ServicesHook):
self.urlpatterns = urlpatterns self.urlpatterns = urlpatterns
self.service_url = settings.IPS4_URL self.service_url = settings.IPS4_URL
self.access_perm = 'ips4.access_ips4' self.access_perm = 'ips4.access_ips4'
self.name_format = '{character_name}'
@property @property
def title(self): def title(self):

View File

@ -1,6 +1,7 @@
from django.conf import settings from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from allianceauth.services.hooks import NameFormatter
from .manager import Ips4Manager from .manager import Ips4Manager
from .models import Ips4User from .models import Ips4User
@ -33,3 +34,8 @@ class Ips4Tasks:
def disable(): def disable():
logging.debug("Deleting all IPS4 users") logging.debug("Deleting all IPS4 users")
Ips4User.objects.all().delete() 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) logger.debug("activate_ips4 called by user %s" % request.user)
character = request.user.profile.main_character character = request.user.profile.main_character
logger.debug("Adding IPS4 user for user %s with main character %s" % (request.user, 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 empty we failed
if result[0] != "" and not Ips4Tasks.has_account(request.user): if result[0] != "" and not Ips4Tasks.has_account(request.user):
ips_user = Ips4User.objects.create(user=request.user, id=result[2], username=result[0]) 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.service_url = settings.MUMBLE_URL
self.access_perm = 'mumble.access_mumble' self.access_perm = 'mumble.access_mumble'
self.service_ctrl_template = 'services/mumble/mumble_service_ctrl.html' 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): def delete_user(self, user, notify_user=False):
logging.debug("Deleting user %s %s account" % (user, self.name)) logging.debug("Deleting user %s %s account" % (user, self.name))

View File

@ -26,25 +26,14 @@ class MumbleManager:
def __generate_random_pass(): def __generate_random_pass():
return ''.join([random.choice(string.ascii_letters + string.digits) for n in range(16)]) 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 @classmethod
def _gen_pwhash(cls, password): def _gen_pwhash(cls, password):
return bcrypt_sha256.encrypt(password.encode('utf-8')) return bcrypt_sha256.encrypt(password.encode('utf-8'))
@classmethod @classmethod
def create_user(cls, user, corp_ticker, username, blue=False): def create_user(cls, user, username):
logger.debug("Creating%s mumble user with username %s and ticker %s" % (' blue' if blue else '', logger.debug("Creating mumble user with username %s" % (username))
username, corp_ticker)) username_clean = cls.__santatize_username(username)
username_clean = cls.__santatize_username(
cls.__generate_username_blue(username, corp_ticker) if blue else
cls.__generate_username(username, corp_ticker))
password = cls.__generate_random_pass() password = cls.__generate_random_pass()
pwhash = cls._gen_pwhash(password) pwhash = cls._gen_pwhash(password)
logger.debug("Proceeding with mumble user creation: clean username %s, pwhash starts with %s" % ( 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.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from allianceauth.services.hooks import NameFormatter
from allianceauth.celery import app from allianceauth.celery import app
from .manager import MumbleManager from .manager import MumbleManager
@ -26,6 +27,11 @@ class MumbleTasks:
logger.info("Deleting all MumbleUser models") logger.info("Deleting all MumbleUser models")
MumbleUser.objects.all().delete() MumbleUser.objects.all().delete()
@staticmethod
def get_username(user):
from .auth_hooks import MumbleService
return NameFormatter(MumbleService(), user).format_name()
@staticmethod @staticmethod
@app.task(bind=True, name="mumble.update_groups") @app.task(bind=True, name="mumble.update_groups")
def update_groups(self, pk): def update_groups(self, pk):

View File

@ -18,10 +18,9 @@ ACCESS_PERM = 'mumble.access_mumble'
def activate_mumble(request): def activate_mumble(request):
logger.debug("activate_mumble called by user %s" % request.user) logger.debug("activate_mumble called by user %s" % request.user)
character = request.user.profile.main_character character = request.user.profile.main_character
ticker = character.corporation_ticker
logger.debug("Adding mumble user for %s with main character %s" % (request.user, character)) 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: if result:
logger.debug("Updated authserviceinfo for user %s with mumble credentials. Updating groups." % request.user) 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.urlpatterns = urlpatterns
self.service_url = settings.JABBER_URL self.service_url = settings.JABBER_URL
self.access_perm = 'openfire.access_openfire' self.access_perm = 'openfire.access_openfire'
self.name_format = '{character_name}'
@property @property
def title(self): def title(self):

View File

@ -6,6 +6,7 @@ from allianceauth.notifications import notify
from allianceauth.celery import app from allianceauth.celery import app
from allianceauth.services.modules.openfire.manager import OpenfireManager from allianceauth.services.modules.openfire.manager import OpenfireManager
from allianceauth.services.hooks import NameFormatter
from .models import OpenfireUser from .models import OpenfireUser
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -65,3 +66,8 @@ class OpenfireTasks:
logger.debug("Updating ALL jabber groups") logger.debug("Updating ALL jabber groups")
for openfire_user in OpenfireUser.objects.exclude(username__exact=''): for openfire_user in OpenfireUser.objects.exclude(username__exact=''):
OpenfireTasks.update_groups.delay(openfire_user.user.pk) 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) logger.debug("activate_jabber called by user %s" % request.user)
character = request.user.profile.main_character character = request.user.profile.main_character
logger.debug("Adding jabber user for user %s with main character %s" % (request.user, 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 our username is blank means we already had a user
if info[0] is not "": if info[0] is not "":
OpenfireUser.objects.update_or_create(user=request.user, defaults={'username': info[0]}) 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.urlpatterns = urlpatterns
self.service_url = settings.PHPBB3_URL self.service_url = settings.PHPBB3_URL
self.access_perm = 'phpbb3.access_phpbb3' self.access_perm = 'phpbb3.access_phpbb3'
self.name_format = '{character_name}'
@property @property
def title(self): def title(self):

View File

@ -5,6 +5,7 @@ from django.core.exceptions import ObjectDoesNotExist
from allianceauth.celery import app from allianceauth.celery import app
from allianceauth.notifications import notify from allianceauth.notifications import notify
from allianceauth.services.hooks import NameFormatter
from .manager import Phpbb3Manager from .manager import Phpbb3Manager
from .models import Phpbb3User from .models import Phpbb3User
@ -64,3 +65,8 @@ class Phpbb3Tasks:
@staticmethod @staticmethod
def disable(): def disable():
Phpbb3User.objects.all().delete() 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 # Valid now we get the main characters
character = request.user.profile.main_character character = request.user.profile.main_character
logger.debug("Adding phpbb user for user %s with main character %s" % (request.user, 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) character.character_id)
# if empty we failed # if empty we failed
if result[0] != "": if result[0] != "":

View File

@ -18,6 +18,7 @@ class SeatService(ServicesHook):
self.name = 'seat' self.name = 'seat'
self.service_url = settings.SEAT_URL self.service_url = settings.SEAT_URL
self.access_perm = 'seat.access_seat' self.access_perm = 'seat.access_seat'
self.name_format = '{character_name}'
@property @property
def title(self): def title(self):

View File

@ -5,6 +5,7 @@ from django.core.exceptions import ObjectDoesNotExist
from allianceauth.celery import app from allianceauth.celery import app
from allianceauth.notifications import notify from allianceauth.notifications import notify
from allianceauth.services.hooks import NameFormatter
from .manager import SeatManager from .manager import SeatManager
from .models import SeatUser from .models import SeatUser
@ -64,3 +65,8 @@ class SeatTasks:
@staticmethod @staticmethod
def deactivate(): def deactivate():
SeatUser.objects.all().delete() 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) stat = SeatManager.check_user_status(character.character_name)
if stat == {}: if stat == {}:
logger.debug("User not found, adding SeAT user for user %s with main character %s" % (request.user, character)) 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: else:
logger.debug("User found, resetting password") logger.debug("User found, resetting password")
username = SeatManager.enable_user(stat["name"]) username = SeatManager.enable_user(stat["name"])

View File

@ -18,6 +18,7 @@ class SmfService(ServicesHook):
self.urlpatterns = urlpatterns self.urlpatterns = urlpatterns
self.service_url = settings.SMF_URL self.service_url = settings.SMF_URL
self.access_perm = 'smf.access_smf' self.access_perm = 'smf.access_smf'
self.name_format = '{character_name}'
@property @property
def title(self): def title(self):

View File

@ -5,6 +5,7 @@ from django.core.exceptions import ObjectDoesNotExist
from allianceauth.celery import app from allianceauth.celery import app
from allianceauth.notifications import notify from allianceauth.notifications import notify
from allianceauth.services.hooks import NameFormatter
from .manager import SmfManager from .manager import SmfManager
from .models import SmfUser from .models import SmfUser
@ -64,3 +65,8 @@ class SmfTasks:
logger.debug("Updating ALL smf groups") logger.debug("Updating ALL smf groups")
for user in SmfUser.objects.exclude(username__exact=''): for user in SmfUser.objects.exclude(username__exact=''):
SmfTasks.update_groups.delay(user.user_id) 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 # Valid now we get the main characters
character = request.user.profile.main_character character = request.user.profile.main_character
logger.debug("Adding smf user for user %s with main character %s" % (request.user, 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 empty we failed
if result[0] != "": if result[0] != "":
SmfUser.objects.update_or_create(user=request.user, defaults={'username': 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.urlpatterns = urlpatterns
self.service_ctrl_template = 'services/teamspeak3/teamspeak3_service_ctrl.html' self.service_ctrl_template = 'services/teamspeak3/teamspeak3_service_ctrl.html'
self.access_perm = 'teamspeak3.access_teamspeak3' self.access_perm = 'teamspeak3.access_teamspeak3'
self.name_format = '[{corp_ticker}]{character_name}'
def delete_user(self, user, notify_user=False): def delete_user(self, user, notify_user=False):
logger.debug('Deleting user %s %s account' % (user, self.name)) logger.debug('Deleting user %s %s account' % (user, self.name))

View File

@ -50,11 +50,6 @@ class Teamspeak3Manager:
sanatized = sanatized.replace("'", "-") sanatized = sanatized.replace("'", "-")
return sanatized return sanatized
@staticmethod
def __generate_username(username, corp_ticker):
sanatized = "[" + corp_ticker + "]" + username
return sanatized[:30]
def _get_userid(self, uid): def _get_userid(self, uid):
logger.debug("Looking for uid %s on TS3 server." % uid) logger.debug("Looking for uid %s on TS3 server." % uid)
try: try:
@ -184,8 +179,8 @@ class Teamspeak3Manager:
except: except:
logger.exception("An unhandled exception has occured while syncing TS groups.") logger.exception("An unhandled exception has occured while syncing TS groups.")
def add_user(self, username, corp_ticker): def add_user(self, username):
username_clean = self.__santatize_username(self.__generate_username(username, corp_ticker)) username_clean = self.__santatize_username(username[:30])
logger.debug("Adding user to TS3 server with cleaned username %s" % username_clean) logger.debug("Adding user to TS3 server with cleaned username %s" % username_clean)
server_groups = self._group_list() server_groups = self._group_list()

View File

@ -5,6 +5,7 @@ from django.core.exceptions import ObjectDoesNotExist
from allianceauth.celery import app from allianceauth.celery import app
from allianceauth.notifications import notify from allianceauth.notifications import notify
from allianceauth.services.hooks import NameFormatter
from .manager import Teamspeak3Manager from .manager import Teamspeak3Manager
from .models import AuthTS, TSgroup, UserTSgroup, Teamspeak3User from .models import AuthTS, TSgroup, UserTSgroup, Teamspeak3User
from .util.ts3 import TeamspeakError from .util.ts3 import TeamspeakError
@ -85,3 +86,8 @@ class Teamspeak3Tasks:
logger.debug("Updating ALL teamspeak3 groups") logger.debug("Updating ALL teamspeak3 groups")
for user in Teamspeak3User.objects.exclude(uid__exact=''): for user in Teamspeak3User.objects.exclude(uid__exact=''):
Teamspeak3Tasks.update_groups.delay(user.user_id) 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 ticker = character.corporation_ticker
with Teamspeak3Manager() as ts3man: with Teamspeak3Manager() as ts3man:
logger.debug("Adding TS3 user for user %s with main character %s" % (request.user, character)) 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 its empty we failed
if result[0] is not "": if result[0] is not "":

View File

@ -16,6 +16,7 @@ class XenforoService(ServicesHook):
self.name = 'xenforo' self.name = 'xenforo'
self.urlpatterns = urlpatterns self.urlpatterns = urlpatterns
self.access_perm = 'xenforo.access_xenforo' self.access_perm = 'xenforo.access_xenforo'
self.name_format = '{character_name}'
@property @property
def title(self): def title(self):

View File

@ -3,6 +3,7 @@ import logging
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from allianceauth.notifications import notify from allianceauth.notifications import notify
from allianceauth.services.hooks import NameFormatter
from .manager import XenForoManager from .manager import XenForoManager
from .models import XenforoUser from .models import XenforoUser
@ -35,3 +36,8 @@ class XenforoTasks:
def disable(cls): def disable(cls):
logger.debug("Deleting ALL XenForo users") logger.debug("Deleting ALL XenForo users")
XenforoUser.objects.all().delete() 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) logger.debug("activate_xenforo_forum called by user %s" % request.user)
character = request.user.profile.main_character character = request.user.profile.main_character
logger.debug("Adding XenForo user for user %s with main character %s" % (request.user, 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 # Based on XenAPI's response codes
if result['response']['status_code'] == 200: if result['response']['status_code'] == 200:
XenforoUser.objects.update_or_create(user=request.user, defaults={'username': result['username']}) 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 corpstats
groups groups
permissions_tool 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.
```