diff --git a/api/funkwhale_api/manage/serializers.py b/api/funkwhale_api/manage/serializers.py index de7d7d63b..256dc20ab 100644 --- a/api/funkwhale_api/manage/serializers.py +++ b/api/funkwhale_api/manage/serializers.py @@ -98,12 +98,28 @@ class ManageUserSerializer(serializers.ModelSerializer): class ManageInvitationSerializer(serializers.ModelSerializer): users = ManageUserSimpleSerializer(many=True, required=False) owner = ManageUserSimpleSerializer(required=False) + invited_user = ManageUserSimpleSerializer(required=False) code = serializers.CharField(required=False, allow_null=True) class Meta: model = users_models.Invitation - fields = ("id", "owner", "code", "expiration_date", "creation_date", "users") - read_only_fields = ["id", "expiration_date", "owner", "creation_date", "users"] + fields = ( + "id", + "owner", + "invited_user", + "code", + "expiration_date", + "creation_date", + "users", + ) + read_only_fields = [ + "id", + "expiration_date", + "owner", + "invited_user", + "creation_date", + "users", + ] def validate_code(self, value): if not value: diff --git a/api/funkwhale_api/users/factories.py b/api/funkwhale_api/users/factories.py index 7b1504328..74c0faa1e 100644 --- a/api/funkwhale_api/users/factories.py +++ b/api/funkwhale_api/users/factories.py @@ -57,6 +57,9 @@ class InvitationFactory(NoUpdateOnCreate, factory.django.DjangoModelFactory): class Params: expired = factory.Trait(expiration_date=factory.LazyFunction(timezone.now)) + with_invited_user = factory.Trait( + invited_user=factory.SubFactory("funkwhale_api.users.factories.UserFactory") + ) class PasswordSetter(factory.PostGenerationMethodCall): diff --git a/api/funkwhale_api/users/migrations/0022_auto_20221119_1819.py b/api/funkwhale_api/users/migrations/0022_auto_20221119_1819.py new file mode 100644 index 000000000..5643a5e41 --- /dev/null +++ b/api/funkwhale_api/users/migrations/0022_auto_20221119_1819.py @@ -0,0 +1,37 @@ +# Generated by Django 3.2.16 on 2022-11-19 18:19 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + +def update_invitation_table(apps, schema_editor): + User = apps.get_model("users", "User") + Invitation = apps.get_model("users", "Invitation") + for user in User.objects.all(): + if user.invitation: + Invitation.objects.filter(id=user.invitation.id).update(invited_user_id=user.id) + + +def rewind(*args, **kwargs): + pass + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0021_auto_20210703_1810'), + ] + + operations = [ + migrations.AddField( + model_name='invitation', + name='invited_user', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='user_invitations', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='invitation', + name='owner', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invitations', to=settings.AUTH_USER_MODEL), + ), + migrations.RunPython(update_invitation_table, rewind), + ] diff --git a/api/funkwhale_api/users/models.py b/api/funkwhale_api/users/models.py index a07b92efa..2ffb523a7 100644 --- a/api/funkwhale_api/users/models.py +++ b/api/funkwhale_api/users/models.py @@ -338,6 +338,9 @@ class Invitation(models.Model): owner = models.ForeignKey( User, related_name="invitations", on_delete=models.CASCADE ) + invited_user = models.ForeignKey( + User, related_name="user_invitations", null=True, on_delete=models.CASCADE + ) code = models.CharField(max_length=50, unique=True) objects = InvitationQuerySet.as_manager() @@ -352,6 +355,10 @@ class Invitation(models.Model): return super().save(**kwargs) + def set_invited_user(self, user): + self.invited_user = user + super().save() + class Application(oauth2_models.AbstractApplication): scope = models.TextField(blank=True) diff --git a/api/funkwhale_api/users/views.py b/api/funkwhale_api/users/views.py index b9891e594..9be44883b 100644 --- a/api/funkwhale_api/users/views.py +++ b/api/funkwhale_api/users/views.py @@ -40,6 +40,9 @@ class RegisterView(registration_views.RegisterView): if not user.is_active: # manual approval, we need to send the confirmation e-mail by hand authentication.send_email_confirmation(self.request, user) + if user.invitation: + user.invitation.set_invited_user(user) + return user diff --git a/api/tests/users/test_models.py b/api/tests/users/test_models.py index 497eedcec..270400edd 100644 --- a/api/tests/users/test_models.py +++ b/api/tests/users/test_models.py @@ -105,6 +105,13 @@ def test_invitation_generates_random_code_on_save(factories): assert len(invitation.code) >= 6 +def test_invitation_get_deleted_when_user_is_deleted(factories): + invitation = factories["users.Invitation"](with_invited_user=True) + invitation.invited_user.delete() + + assert models.Invitation.objects.count() == 0 + + def test_invitation_expires_after_delay(factories, settings): delay = settings.USERS_INVITATION_EXPIRATION_DAYS invitation = factories["users.Invitation"]() diff --git a/changes/changelog.d/1952.bugfix b/changes/changelog.d/1952.bugfix new file mode 100644 index 000000000..e16def23d --- /dev/null +++ b/changes/changelog.d/1952.bugfix @@ -0,0 +1 @@ +Fixed invitation reuse after the invited user has been deleted (#1952) diff --git a/front/src/components/manage/users/InvitationsTable.vue b/front/src/components/manage/users/InvitationsTable.vue index b3b64cabc..39de7aaed 100644 --- a/front/src/components/manage/users/InvitationsTable.vue +++ b/front/src/components/manage/users/InvitationsTable.vue @@ -75,6 +75,11 @@ Owner +