Merge branch 'mumble_displaynames' into 'master'

Mumble Display Names

See merge request allianceauth/allianceauth!1185
This commit is contained in:
Ariel Rin 2020-04-06 02:19:53 +00:00
commit c31cc4dbee
9 changed files with 185 additions and 22 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -19,5 +19,6 @@ install_command = pip install -e ".[testing]" -U {opts} {packages}
commands = commands =
all: coverage run runtests.py -v 2 all: coverage run runtests.py -v 2
all: coverage report -m all: coverage report -m
all: coverage xml
core: coverage run runtests.py allianceauth.authentication.tests.test_app_settings -v 2 core: coverage run runtests.py allianceauth.authentication.tests.test_app_settings -v 2
all: coverage xml all: coverage xml