Allow reconnecting characters to old users.

Addresses #1007
This commit is contained in:
Adarnof 2018-04-14 15:13:42 -04:00
parent df3acccc50
commit 319cba8653
6 changed files with 104 additions and 8 deletions

View File

@ -6,7 +6,7 @@ from django.db.models import Q
from allianceauth.services.hooks import ServicesHook from allianceauth.services.hooks import ServicesHook
from django.db.models.signals import pre_save, post_save, pre_delete, post_delete, m2m_changed from django.db.models.signals import pre_save, post_save, pre_delete, post_delete, m2m_changed
from django.dispatch import receiver from django.dispatch import receiver
from allianceauth.authentication.models import State, get_guest_state, CharacterOwnership, UserProfile from allianceauth.authentication.models import State, get_guest_state, CharacterOwnership, UserProfile, OwnershipRecord
from allianceauth.hooks import get_hooks from allianceauth.hooks import get_hooks
from allianceauth.eveonline.models import EveCharacter from allianceauth.eveonline.models import EveCharacter
from django.forms import ModelForm from django.forms import ModelForm
@ -160,12 +160,23 @@ class StateAdmin(admin.ModelAdmin):
return obj.userprofile_set.all().count() return obj.userprofile_set.all().count()
@admin.register(CharacterOwnership) class BaseOwnershipAdmin(admin.ModelAdmin):
class CharacterOwnershipAdmin(admin.ModelAdmin):
list_display = ('user', 'character') list_display = ('user', 'character')
search_fields = ('user__username', 'character__character_name', 'character__corporation_name', 'character__alliance_name') search_fields = ('user__username', 'character__character_name', 'character__corporation_name', 'character__alliance_name')
readonly_fields = ('owner_hash', 'character')
def get_readonly_fields(self, request, obj=None):
if obj and obj.pk:
return 'owner_hash', 'character'
return tuple()
@admin.register(OwnershipRecord)
class OwnershipRecordAdmin(BaseOwnershipAdmin):
list_display = BaseOwnershipAdmin.list_display + ('created',)
@admin.register(CharacterOwnership)
class CharacterOwnershipAdmin(BaseOwnershipAdmin):
def has_add_permission(self, request): def has_add_permission(self, request):
return False return False

View File

@ -2,7 +2,7 @@ from django.contrib.auth.backends import ModelBackend
from django.contrib.auth.models import Permission from django.contrib.auth.models import Permission
from django.contrib.auth.models import User from django.contrib.auth.models import User
from .models import UserProfile, CharacterOwnership from .models import UserProfile, CharacterOwnership, OwnershipRecord
class StateBackend(ModelBackend): class StateBackend(ModelBackend):
@ -43,7 +43,18 @@ class StateBackend(ModelBackend):
CharacterOwnership.objects.create_by_token(token) CharacterOwnership.objects.create_by_token(token)
return profile.user return profile.user
except UserProfile.DoesNotExist: except UserProfile.DoesNotExist:
pass # now we check historical records to see if this is a returning user
records = OwnershipRecord.objects.filter(owner_hash=token.character_owner_hash).filter(character__character_id=token.character_id)
if records.exists():
# we've seen this character owner before. Re-attach to their old user account
user = records[0].user
token.user = user
co = CharacterOwnership.objects.create_by_token(token)
if not user.profile.main_character:
# set this as their main by default if they have none
user.profile.main_character = co.character
user.profile.save()
return user
return self.create_user(token) return self.create_user(token)
def create_user(self, token): def create_user(self, token):

View File

@ -0,0 +1,40 @@
# Generated by Django 2.0.4 on 2018-04-14 18:28
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
def create_initial_records(apps, schema_editor):
OwnershipRecord = apps.get_model('authentication', 'OwnershipRecord')
CharacterOwnership = apps.get_model('authentication', 'CharacterOwnership')
OwnershipRecord.objects.bulk_create([
OwnershipRecord(user=o.user, character=o.character, owner_hash=o.owner_hash) for o in CharacterOwnership.objects.all()
])
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('eveonline', '0009_on_delete'),
('authentication', '0015_user_profiles'),
]
operations = [
migrations.CreateModel(
name='OwnershipRecord',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('owner_hash', models.CharField(db_index=True, max_length=28)),
('created', models.DateTimeField(auto_now=True)),
('character', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ownership_records', to='eveonline.EveCharacter')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ownership_records', to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['-created'],
},
),
migrations.RunPython(create_initial_records, migrations.RunPython.noop)
]

View File

@ -96,3 +96,16 @@ class CharacterOwnership(models.Model):
def __str__(self): def __str__(self):
return "%s: %s" % (self.user, self.character) return "%s: %s" % (self.user, self.character)
class OwnershipRecord(models.Model):
character = models.ForeignKey(EveCharacter, on_delete=models.CASCADE, related_name='ownership_records')
owner_hash = models.CharField(max_length=28, db_index=True)
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='ownership_records')
created = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['-created']
def __str__(self):
return "%s: %s on %s" % (self.user, self.character, self.created)

View File

@ -1,6 +1,6 @@
import logging import logging
from .models import CharacterOwnership, UserProfile, get_guest_state, State from .models import CharacterOwnership, UserProfile, get_guest_state, State, OwnershipRecord
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db.models import Q from django.db.models import Q
from django.db.models.signals import pre_save, post_save, pre_delete, post_delete, m2m_changed from django.db.models.signals import pre_save, post_save, pre_delete, post_delete, m2m_changed
@ -153,3 +153,15 @@ def check_state_on_character_update(sender, instance, *args, **kwargs):
except UserProfile.DoesNotExist: except UserProfile.DoesNotExist:
logger.debug("Character {0} is not a main character. No state assessment required.".format(instance)) logger.debug("Character {0} is not a main character. No state assessment required.".format(instance))
pass pass
@receiver(post_save, sender=CharacterOwnership)
def ownership_record_creation(sender, instance, created, *args, **kwargs):
if created:
records = OwnershipRecord.objects.filter(owner_hash=instance.owner_hash).filter(character=instance.character)
if records.exists():
if records[0].user == instance.user: # most recent record is sorted first
logger.debug("Already have ownership record of {0} by user {1}".format(instance.character, instance.user))
return
logger.info("Character {0} has a new owner {1}. Creating ownership record.".format(instance.character, instance.user))
OwnershipRecord.objects.create(user=instance.user, character=instance.character, owner_hash=instance.owner_hash)

View File

@ -3,7 +3,7 @@ from unittest import mock
from django.test import TestCase from django.test import TestCase
from django.contrib.auth.models import User from django.contrib.auth.models import User
from allianceauth.tests.auth_utils import AuthUtils from allianceauth.tests.auth_utils import AuthUtils
from .models import CharacterOwnership, UserProfile, State, get_guest_state from .models import CharacterOwnership, UserProfile, State, get_guest_state, OwnershipRecord
from .backends import StateBackend from .backends import StateBackend
from .tasks import check_character_ownership from .tasks import check_character_ownership
from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo, EveAllianceInfo from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo, EveAllianceInfo
@ -90,6 +90,7 @@ class BackendTestCase(TestCase):
corporation_ticker='CORP', corporation_ticker='CORP',
) )
cls.user = AuthUtils.create_user('test_user', disconnect_signals=True) cls.user = AuthUtils.create_user('test_user', disconnect_signals=True)
cls.old_user = AuthUtils.create_user('old_user', disconnect_signals=True)
AuthUtils.disconnect_signals() AuthUtils.disconnect_signals()
CharacterOwnership.objects.create(user=cls.user, character=cls.main_character, owner_hash='1') CharacterOwnership.objects.create(user=cls.user, character=cls.main_character, owner_hash='1')
CharacterOwnership.objects.create(user=cls.user, character=cls.alt_character, owner_hash='2') CharacterOwnership.objects.create(user=cls.user, character=cls.alt_character, owner_hash='2')
@ -113,6 +114,14 @@ class BackendTestCase(TestCase):
self.assertEqual(user.username, 'Unclaimed_Character') self.assertEqual(user.username, 'Unclaimed_Character')
self.assertEqual(user.profile.main_character, self.unclaimed_character) self.assertEqual(user.profile.main_character, self.unclaimed_character)
def test_authenticate_character_record(self):
t = Token(character_id=self.unclaimed_character.character_id, character_name=self.unclaimed_character.character_name, character_owner_hash='4')
record = OwnershipRecord.objects.create(user=self.old_user, character=self.unclaimed_character, owner_hash='4')
user = StateBackend().authenticate(t)
self.assertEqual(user, self.old_user)
self.assertTrue(CharacterOwnership.objects.filter(owner_hash='4', user=self.old_user).exists())
self.assertTrue(user.profile.main_character)
def test_iterate_username(self): def test_iterate_username(self):
t = Token(character_id=self.unclaimed_character.character_id, t = Token(character_id=self.unclaimed_character.character_id,
character_name=self.unclaimed_character.character_name, character_owner_hash='3') character_name=self.unclaimed_character.character_name, character_owner_hash='3')