diff --git a/allianceauth/authentication/admin.py b/allianceauth/authentication/admin.py index 61183766..894ab2f5 100644 --- a/allianceauth/authentication/admin.py +++ b/allianceauth/authentication/admin.py @@ -6,7 +6,7 @@ from django.db.models import Q from allianceauth.services.hooks import ServicesHook from django.db.models.signals import pre_save, post_save, pre_delete, post_delete, m2m_changed 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.eveonline.models import EveCharacter from django.forms import ModelForm @@ -160,12 +160,23 @@ class StateAdmin(admin.ModelAdmin): return obj.userprofile_set.all().count() -@admin.register(CharacterOwnership) -class CharacterOwnershipAdmin(admin.ModelAdmin): +class BaseOwnershipAdmin(admin.ModelAdmin): list_display = ('user', 'character') 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): return False diff --git a/allianceauth/authentication/backends.py b/allianceauth/authentication/backends.py index 281c7984..a37e4afd 100644 --- a/allianceauth/authentication/backends.py +++ b/allianceauth/authentication/backends.py @@ -2,7 +2,7 @@ from django.contrib.auth.backends import ModelBackend from django.contrib.auth.models import Permission from django.contrib.auth.models import User -from .models import UserProfile, CharacterOwnership +from .models import UserProfile, CharacterOwnership, OwnershipRecord class StateBackend(ModelBackend): @@ -43,7 +43,18 @@ class StateBackend(ModelBackend): CharacterOwnership.objects.create_by_token(token) return profile.user 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) def create_user(self, token): diff --git a/allianceauth/authentication/migrations/0016_ownershiprecord.py b/allianceauth/authentication/migrations/0016_ownershiprecord.py new file mode 100644 index 00000000..b2df6d8d --- /dev/null +++ b/allianceauth/authentication/migrations/0016_ownershiprecord.py @@ -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) + ] diff --git a/allianceauth/authentication/models.py b/allianceauth/authentication/models.py index 84b5dc58..aa565219 100755 --- a/allianceauth/authentication/models.py +++ b/allianceauth/authentication/models.py @@ -96,3 +96,16 @@ class CharacterOwnership(models.Model): def __str__(self): 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) \ No newline at end of file diff --git a/allianceauth/authentication/signals.py b/allianceauth/authentication/signals.py index e774a2fc..d8e73fdf 100644 --- a/allianceauth/authentication/signals.py +++ b/allianceauth/authentication/signals.py @@ -1,6 +1,6 @@ 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.db.models import Q 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: logger.debug("Character {0} is not a main character. No state assessment required.".format(instance)) 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) \ No newline at end of file diff --git a/allianceauth/authentication/tests.py b/allianceauth/authentication/tests.py index ae1d119c..fb63f52e 100644 --- a/allianceauth/authentication/tests.py +++ b/allianceauth/authentication/tests.py @@ -3,7 +3,7 @@ from unittest import mock from django.test import TestCase from django.contrib.auth.models import User 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 .tasks import check_character_ownership from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo, EveAllianceInfo @@ -90,6 +90,7 @@ class BackendTestCase(TestCase): corporation_ticker='CORP', ) cls.user = AuthUtils.create_user('test_user', disconnect_signals=True) + cls.old_user = AuthUtils.create_user('old_user', disconnect_signals=True) AuthUtils.disconnect_signals() 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') @@ -113,6 +114,14 @@ class BackendTestCase(TestCase): self.assertEqual(user.username, '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): t = Token(character_id=self.unclaimed_character.character_id, character_name=self.unclaimed_character.character_name, character_owner_hash='3')