diff --git a/api/config/settings/common.py b/api/config/settings/common.py index a836dfdfddb096384bfff92855a34cf478f0aaa6..b74c2bdfe499af75053748ddb8d90b96b8b42760 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 eed8c7175a2dacdb65404aaf827c2bee04582dbb..5fceb57bbc17bdd8ac70e95bc81e672869b4abe8 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 0000000000000000000000000000000000000000..e8204c4e4748371b7906d9115b0d529f30a35db7 --- /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 15d16db2369967cddf38310d871c6bae3733dab0..61f57a3c55dcced4af65586b2c20012b6af6f391 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 74bb091e54270c4034050b7810e2b6ba3cb4bde3..475691293dd347cd81946743994cb20fef62be09 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]