From 789bef38cb2f24f2f0bd13c891fb238460cfb07f Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Tue, 19 Jun 2018 21:47:43 +0200 Subject: [PATCH] See #248: model / migration --- api/config/settings/common.py | 4 ++ api/funkwhale_api/users/factories.py | 15 ++++++ .../migrations/0009_auto_20180619_2024.py | 31 +++++++++++++ api/funkwhale_api/users/models.py | 46 +++++++++++++++++++ api/tests/users/test_models.py | 23 ++++++++++ 5 files changed, 119 insertions(+) create mode 100644 api/funkwhale_api/users/migrations/0009_auto_20180619_2024.py diff --git a/api/config/settings/common.py b/api/config/settings/common.py index a836dfdfd..b74c2bdfe 100644 --- a/api/config/settings/common.py +++ b/api/config/settings/common.py @@ -461,3 +461,7 @@ MUSIC_DIRECTORY_PATH = env("MUSIC_DIRECTORY_PATH", default=None) MUSIC_DIRECTORY_SERVE_PATH = env( "MUSIC_DIRECTORY_SERVE_PATH", default=MUSIC_DIRECTORY_PATH ) + +USERS_INVITATION_EXPIRATION_DAYS = env.int( + "USERS_INVITATION_EXPIRATION_DAYS", default=14 +) diff --git a/api/funkwhale_api/users/factories.py b/api/funkwhale_api/users/factories.py index eed8c7175..5fceb57bb 100644 --- a/api/funkwhale_api/users/factories.py +++ b/api/funkwhale_api/users/factories.py @@ -1,5 +1,6 @@ import factory from django.contrib.auth.models import Permission +from django.utils import timezone from funkwhale_api.factories import ManyToManyFromList, registry @@ -28,6 +29,17 @@ class GroupFactory(factory.django.DjangoModelFactory): self.permissions.add(*perms) +@registry.register +class InvitationFactory(factory.django.DjangoModelFactory): + owner = factory.LazyFunction(lambda: UserFactory()) + + class Meta: + model = "users.Invitation" + + class Params: + expired = factory.Trait(expiration_date=factory.LazyFunction(timezone.now)) + + @registry.register class UserFactory(factory.django.DjangoModelFactory): username = factory.Sequence(lambda n: "user-{0}".format(n)) @@ -40,6 +52,9 @@ class UserFactory(factory.django.DjangoModelFactory): model = "users.User" django_get_or_create = ("username",) + class Params: + invited = factory.Trait(invitation=factory.SubFactory(InvitationFactory)) + @factory.post_generation def perms(self, create, extracted, **kwargs): if not create: diff --git a/api/funkwhale_api/users/migrations/0009_auto_20180619_2024.py b/api/funkwhale_api/users/migrations/0009_auto_20180619_2024.py new file mode 100644 index 000000000..e8204c4e4 --- /dev/null +++ b/api/funkwhale_api/users/migrations/0009_auto_20180619_2024.py @@ -0,0 +1,31 @@ +# Generated by Django 2.0.6 on 2018-06-19 20:24 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0008_auto_20180617_1531'), + ] + + operations = [ + migrations.CreateModel( + name='Invitation', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('creation_date', models.DateTimeField(default=django.utils.timezone.now)), + ('expiration_date', models.DateTimeField()), + ('code', models.CharField(max_length=50, unique=True)), + ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invitations', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.AddField( + model_name='user', + name='invitation', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='users', to='users.Invitation'), + ), + ] diff --git a/api/funkwhale_api/users/models.py b/api/funkwhale_api/users/models.py index 15d16db23..61f57a3c5 100644 --- a/api/funkwhale_api/users/models.py +++ b/api/funkwhale_api/users/models.py @@ -4,6 +4,8 @@ from __future__ import absolute_import, unicode_literals import binascii import datetime import os +import random +import string import uuid from django.conf import settings @@ -79,6 +81,14 @@ class User(AbstractUser): last_activity = models.DateTimeField(default=None, null=True, blank=True) + invitation = models.ForeignKey( + "Invitation", + related_name="users", + null=True, + blank=True, + on_delete=models.SET_NULL, + ) + def __str__(self): return self.username @@ -138,3 +148,39 @@ class User(AbstractUser): if current is None or current < now - datetime.timedelta(seconds=delay): self.last_activity = now self.save(update_fields=["last_activity"]) + + +def generate_code(length=10): + return "".join( + random.SystemRandom().choice(string.ascii_lowercase) for _ in range(length) + ) + + +class InvitationQuerySet(models.QuerySet): + def open(self): + now = timezone.now() + qs = self.annotate(_users=models.Count("users")) + qs = qs.filter(_users=0) + qs = qs.exclude(expiration_date__lte=now) + return qs + + +class Invitation(models.Model): + creation_date = models.DateTimeField(default=timezone.now) + expiration_date = models.DateTimeField() + owner = models.ForeignKey( + User, related_name="invitations", on_delete=models.CASCADE + ) + code = models.CharField(max_length=50, unique=True) + + objects = InvitationQuerySet.as_manager() + + def save(self, **kwargs): + if not self.code: + self.code = generate_code() + if not self.expiration_date: + self.expiration_date = self.creation_date + datetime.timedelta( + days=settings.USERS_INVITATION_EXPIRATION_DAYS + ) + + return super().save(**kwargs) diff --git a/api/tests/users/test_models.py b/api/tests/users/test_models.py index 74bb091e5..475691293 100644 --- a/api/tests/users/test_models.py +++ b/api/tests/users/test_models.py @@ -1,3 +1,4 @@ +import datetime import pytest from funkwhale_api.users import models @@ -95,3 +96,25 @@ def test_record_activity_does_nothing_if_already(factories, now, mocker): user.record_activity() save.assert_not_called() + + +def test_invitation_generates_random_code_on_save(factories): + invitation = factories["users.Invitation"]() + assert len(invitation.code) >= 6 + + +def test_invitation_expires_after_delay(factories, settings): + delay = settings.USERS_INVITATION_EXPIRATION_DAYS + invitation = factories["users.Invitation"]() + assert invitation.expiration_date == ( + invitation.creation_date + datetime.timedelta(days=delay) + ) + + +def test_can_filter_open_invitations(factories): + okay = factories["users.Invitation"]() + factories["users.Invitation"](expired=True) + factories["users.User"](invited=True) + + assert models.Invitation.objects.count() == 3 + assert list(models.Invitation.objects.open()) == [okay]