Fix state role not always updated due to lazy properties

This commit is contained in:
ErikKalkoken 2020-07-02 21:26:40 +02:00
parent 3bab349d7b
commit 4d546f948d
7 changed files with 57 additions and 23 deletions

View File

@ -73,11 +73,17 @@ class UserProfile(models.Model):
if commit: if commit:
logger.info('Updating {} state to {}'.format(self.user, self.state)) logger.info('Updating {} state to {}'.format(self.user, self.state))
self.save(update_fields=['state']) self.save(update_fields=['state'])
notify(self.user, _('State Changed'), notify(
_('Your user state has been changed to %(state)s') % ({'state': state}), self.user,
'info') _('State Changed'),
_('Your user state has been changed to %(state)s')
% ({'state': state}),
'info'
)
from allianceauth.authentication.signals import state_changed from allianceauth.authentication.signals import state_changed
state_changed.send(sender=self.__class__, user=self.user, state=self.state) state_changed.send(
sender=self.__class__, user=self.user, state=self.state
)
def __str__(self): def __str__(self):
return str(self.user) return str(self.user)

View File

@ -23,9 +23,7 @@ def trigger_state_check(state):
check_states = State.objects.filter(priority__lt=state.priority) check_states = State.objects.filter(priority__lt=state.priority)
for profile in UserProfile.objects.filter(state__in=check_states): for profile in UserProfile.objects.filter(state__in=check_states):
if state.available_to_user(profile.user): if state.available_to_user(profile.user):
profile.state = state profile.assign_state(state)
profile.save(update_fields=['state'])
state_changed.send(sender=state.__class__, user=profile.user, state=state)
@receiver(m2m_changed, sender=State.member_characters.through) @receiver(m2m_changed, sender=State.member_characters.through)

View File

@ -70,6 +70,8 @@ class DiscordService(ServicesHook):
tasks.update_nickname.apply_async( tasks.update_nickname.apply_async(
kwargs={ kwargs={
'user_pk': user.pk, 'user_pk': user.pk,
# since the new nickname is not yet in the DB we need to
# provide it manually to the task
'nickname': DiscordUser.objects.user_formatted_nick(user) 'nickname': DiscordUser.objects.user_formatted_nick(user)
}, },
priority=SINGLE_TASK_PRIORITY priority=SINGLE_TASK_PRIORITY
@ -90,10 +92,16 @@ class DiscordService(ServicesHook):
tasks.update_all_groups.delay() tasks.update_all_groups.delay()
def update_groups(self, user): def update_groups(self, user):
logger.debug('Processing %s groups for %s', self.name, user) logger.debug('Processing %s groups for %s', self.name, user)
if self.user_has_account(user): if self.user_has_account(user):
tasks.update_groups.apply_async( tasks.update_groups.apply_async(
kwargs={'user_pk': user.pk}, priority=SINGLE_TASK_PRIORITY kwargs={
'user_pk': user.pk,
# since state changes may not yet be in the DB we need to
# provide the new state name manually to the task
'state_name': user.profile.state.name
},
priority=SINGLE_TASK_PRIORITY
) )
def update_groups_bulk(self, users: list): def update_groups_bulk(self, users: list):

View File

@ -127,9 +127,17 @@ class DiscordUserManager(models.Manager):
return None return None
@staticmethod @staticmethod
def user_group_names(user: User) -> list: def user_group_names(user: User, state_name: str = None) -> list:
"""returns list of group names plus state the given user is a member of""" """returns list of group names plus state the given user is a member of"""
return [group.name for group in user.groups.all()] + [user.profile.state.name] if not state_name:
state_name = user.profile.state.name
group_names = (
[group.name for group in user.groups.all()] + [state_name]
)
logger.debug(
"Group names for roles updates of user %s are: %s", user, group_names
)
return group_names
def user_has_account(self, user: User) -> bool: def user_has_account(self, user: User) -> bool:
"""Returns True if the user has an Discord account, else False """Returns True if the user has an Discord account, else False

View File

@ -96,10 +96,13 @@ class DiscordUser(models.Model):
else: else:
return False return False
def update_groups(self) -> bool: def update_groups(self, state_name: str = None) -> bool:
"""update groups for a user based on his current group memberships. """update groups for a user based on his current group memberships.
Will add or remove roles of a user as needed. Will add or remove roles of a user as needed.
Params:
- state_name: optional state name to be used
Returns: Returns:
- True on success - True on success
- None if user is no longer a member of the Discord server - None if user is no longer a member of the Discord server
@ -132,7 +135,9 @@ class DiscordUser(models.Model):
requested_roles = match_or_create_roles_from_names( requested_roles = match_or_create_roles_from_names(
client=client, client=client,
guild_id=DISCORD_GUILD_ID, guild_id=DISCORD_GUILD_ID,
role_names=DiscordUser.objects.user_group_names(self.user) role_names=DiscordUser.objects.user_group_names(
user=self.user, state_name=state_name
)
) )
logger.debug( logger.debug(
'Requested roles for user %s: %s', self.user, requested_roles.ids() 'Requested roles for user %s: %s', self.user, requested_roles.ids()
@ -148,13 +153,13 @@ class DiscordUser(models.Model):
role_ids=list(new_roles.ids()) role_ids=list(new_roles.ids())
) )
if success: if success:
logger.info('Groups for %s have been updated', self.user) logger.info('Roles for %s have been updated', self.user)
else: else:
logger.warning('Failed to update groups for %s', self.user) logger.warning('Failed to update roles for %s', self.user)
return success return success
else: else:
logger.info('No need to update groups for user %s', self.user) logger.info('No need to update roles for user %s', self.user)
return True return True
def update_username(self) -> bool: def update_username(self) -> bool:

View File

@ -6,6 +6,7 @@ from requests.exceptions import HTTPError
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db.models.query import QuerySet from django.db.models.query import QuerySet
from allianceauth.authentication.models import UserProfile
from allianceauth.services.tasks import QueueOnce from allianceauth.services.tasks import QueueOnce
from . import __title__ from . import __title__
@ -26,13 +27,14 @@ BULK_TASK_PRIORITY = 6
@shared_task( @shared_task(
bind=True, name='discord.update_groups', base=QueueOnce, max_retries=None bind=True, name='discord.update_groups', base=QueueOnce, max_retries=None
) )
def update_groups(self, user_pk: int) -> None: def update_groups(self, user_pk: int, state_name: str = None) -> None:
"""Update roles on Discord for given user according to his current groups """Update roles on Discord for given user according to his current groups
Params: Params:
- user_pk: PK of given user - user_pk: PK of given user
""" - state_name: optional state name to be used
_task_perform_user_action(self, user_pk, 'update_groups') """
_task_perform_user_action(self, user_pk, 'update_groups', state_name=state_name)
@shared_task( @shared_task(
@ -76,6 +78,7 @@ def _task_perform_user_action(self, user_pk: int, method: str, **kwargs) -> None
"""perform a user related action incl. managing all exceptions""" """perform a user related action incl. managing all exceptions"""
logger.debug("Starting %s for user with pk %s", method, user_pk) logger.debug("Starting %s for user with pk %s", method, user_pk)
user = User.objects.get(pk=user_pk) user = User.objects.get(pk=user_pk)
# logger.debug("user %s has state %s", user, user.profile.state)
if DiscordUser.objects.user_has_account(user): if DiscordUser.objects.user_has_account(user):
logger.info("Running %s for user %s", method, user) logger.info("Running %s for user %s", method, user)
try: try:

View File

@ -238,15 +238,21 @@ class TestServiceFeatures(TransactionTestCase):
# request mocks # request mocks
requests_mocker.get( requests_mocker.get(
guild_member_request.url, guild_member_request.url,
json={'user': create_user_info(),'roles': ['13', '99']} json={'user': create_user_info(),'roles': ['3', '13', '99']}
) )
requests_mocker.get( requests_mocker.get(
guild_roles_request.url, guild_roles_request.url,
json=[ROLE_ALPHA, ROLE_BRAVO, ROLE_MIKE, ROLE_MEMBER, ROLE_BLUE] json=[
ROLE_ALPHA, ROLE_BRAVO, ROLE_CHARLIE, ROLE_MIKE, ROLE_MEMBER, ROLE_BLUE
]
) )
requests_mocker.post(create_guild_role_request.url, json=ROLE_CHARLIE) requests_mocker.post(create_guild_role_request.url, json=ROLE_CHARLIE)
requests_mocker.patch(modify_guild_member_request.url, status_code=204) requests_mocker.patch(modify_guild_member_request.url, status_code=204)
AuthUtils.disconnect_signals()
self.user.groups.add(self.group_charlie)
AuthUtils.connect_signals()
# demote user to blue state # demote user to blue state
self.blue_state.member_characters.add(self.main) self.blue_state.member_characters.add(self.main)
self.member_state.member_characters.remove(self.main) self.member_state.member_characters.remove(self.main)
@ -257,7 +263,7 @@ class TestServiceFeatures(TransactionTestCase):
my_request = DiscordRequest(r.method, r.url) my_request = DiscordRequest(r.method, r.url)
if my_request == modify_guild_member_request and "roles" in r.json(): if my_request == modify_guild_member_request and "roles" in r.json():
roles_updated = True roles_updated = True
self.assertSetEqual(set(r.json()["roles"]), {13, 98}) self.assertSetEqual(set(r.json()["roles"]), {3, 13, 98})
break break
self.assertTrue(roles_updated) self.assertTrue(roles_updated)