diff --git a/CHANGELOG b/CHANGELOG index a5f7cf23f..5d83c5fc1 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -10,6 +10,48 @@ This changelog is viewable on the web at https://docs.funkwhale.audio/changelog. .. towncrier +1.2.9 (2022-11-25) +------------------ + +Upgrade instructions are available at +https://docs.funkwhale.audio/admin/upgrading.html + +Bugfixes: + +- Ensure index.html files get loaded with UTF-8 encoding +- Fixed invitation reuse after the invited user has been deleted (#1952) +- Fixed unplayable skipped upload (#1349) + + Committers: + +- Georg Krause +- Marcos Peña +- Philipp Wolfer +- Travis Briggs + +Contributors to our Issues: + +- Ciarán Ainsworth +- Georg Krause +- JuniorJPDJ +- Kasper Seweryn +- Marcos Peña +- Mathieu Jourdan +- Micha Gläß-Stöcker +- fuomag9 +- gammelalf +- myOmikron +- petitminion + +Contributors to our Merge Requests: + +- Georg Krause +- JuniorJPDJ +- Marcos Peña +- Philipp Wolfer +- fuomag9 + + 1.2.8 (2022-09-12) ------------------ diff --git a/api/funkwhale_api/__init__.py b/api/funkwhale_api/__init__.py index ced53d79f..55e454961 100644 --- a/api/funkwhale_api/__init__.py +++ b/api/funkwhale_api/__init__.py @@ -1,4 +1,4 @@ -__version__ = "1.2.8" +__version__ = "1.2.9" __version_info__ = tuple( [ int(num) if num.isdigit() else num diff --git a/api/funkwhale_api/common/middleware.py b/api/funkwhale_api/common/middleware.py index 955214e32..5736b973a 100644 --- a/api/funkwhale_api/common/middleware.py +++ b/api/funkwhale_api/common/middleware.py @@ -126,6 +126,7 @@ def get_spa_file(spa_url, name): utils.join_url(spa_url, name), ) response.raise_for_status() + response.encoding = "utf-8" content = response.text caches["local"].set(cache_key, content, settings.FUNKWHALE_SPA_HTML_CACHE_DURATION) return content diff --git a/api/funkwhale_api/federation/signing.py b/api/funkwhale_api/federation/signing.py index 7ab1a5d06..ccac03045 100644 --- a/api/funkwhale_api/federation/signing.py +++ b/api/funkwhale_api/federation/signing.py @@ -30,8 +30,11 @@ def verify_date(raw_date): delta = datetime.timedelta(seconds=DATE_HEADER_VALID_FOR) now = timezone.now() if dt < now - delta or dt > now + delta: + logger.debug( + f"Request Date {raw_date} is too too far in the future or in the past" + ) raise forms.ValidationError( - f"Request Date {raw_date} is too far in the future or in the past" + "Request Date is too far in the future or in the past" ) return dt diff --git a/api/funkwhale_api/manage/serializers.py b/api/funkwhale_api/manage/serializers.py index fe8c9adbd..b96921e33 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/music/management/commands/prune_skipped_uploads.py b/api/funkwhale_api/music/management/commands/prune_skipped_uploads.py index 7dd86f9af..e707c484b 100644 --- a/api/funkwhale_api/music/management/commands/prune_skipped_uploads.py +++ b/api/funkwhale_api/music/management/commands/prune_skipped_uploads.py @@ -21,7 +21,7 @@ class Command(BaseCommand): @transaction.atomic def handle(self, *args, **options): - skipped = models.Uploads.objects.filter(import_status="skipped") + skipped = models.Upload.objects.filter(import_status="skipped") count = len(skipped) if options["force"]: skipped.delete() diff --git a/api/funkwhale_api/music/models.py b/api/funkwhale_api/music/models.py index 81e5ab685..8ce1fdcdd 100644 --- a/api/funkwhale_api/music/models.py +++ b/api/funkwhale_api/music/models.py @@ -668,8 +668,12 @@ class UploadQuerySet(common_models.NullsLastQuerySet): libraries = Library.objects.viewable_by(actor) if include: - return self.filter(library__in=libraries, import_status="finished") - return self.exclude(library__in=libraries, import_status="finished") + return self.filter( + library__in=libraries, import_status__in=["finished", "skipped"] + ) + return self.exclude( + library__in=libraries, import_status__in=["finished", "skipped"] + ) def local(self, include=True): query = models.Q(library__actor__domain_id=settings.FEDERATION_HOSTNAME) diff --git a/api/funkwhale_api/users/factories.py b/api/funkwhale_api/users/factories.py index d60abc0d2..e8b785bb5 100644 --- a/api/funkwhale_api/users/factories.py +++ b/api/funkwhale_api/users/factories.py @@ -58,6 +58,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 55dd3906f..8c233188d 100644 --- a/api/funkwhale_api/users/models.py +++ b/api/funkwhale_api/users/models.py @@ -335,6 +335,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() @@ -349,6 +352,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 3128dfbcf..0771e5d6e 100644 --- a/api/funkwhale_api/users/views.py +++ b/api/funkwhale_api/users/views.py @@ -38,6 +38,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/music/test_models.py b/api/tests/music/test_models.py index 93d95922e..253eca2e7 100644 --- a/api/tests/music/test_models.py +++ b/api/tests/music/test_models.py @@ -211,7 +211,8 @@ def test_library(factories): @pytest.mark.parametrize( - "status,expected", [("pending", False), ("errored", False), ("finished", True)] + "status,expected", + [("pending", False), ("errored", False), ("finished", True), ("skipped", True)], ) def test_playable_by_correct_status(status, expected, factories): upload = factories["music.Upload"]( diff --git a/api/tests/users/test_models.py b/api/tests/users/test_models.py index ef4244597..5a3f9dc55 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/docs/poetry.lock b/docs/poetry.lock index 64fd4f011..44a70a624 100644 --- a/docs/poetry.lock +++ b/docs/poetry.lock @@ -279,7 +279,7 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "setuptools" -version = "65.6.0" +version = "65.6.3" description = "Easily download, build, install, upgrade, and uninstall Python packages" category = "main" optional = false @@ -498,11 +498,11 @@ python-versions = ">=2" [[package]] name = "urllib3" -version = "1.26.12" +version = "1.26.13" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" [package.extras] brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] @@ -688,8 +688,8 @@ requests = [ {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, ] setuptools = [ - {file = "setuptools-65.6.0-py3-none-any.whl", hash = "sha256:6211d2f5eddad8757bd0484923ca7c0a6302ebc4ab32ea5e94357176e0ca0840"}, - {file = "setuptools-65.6.0.tar.gz", hash = "sha256:d1eebf881c6114e51df1664bc2c9133d022f78d12d5f4f665b9191f084e2862d"}, + {file = "setuptools-65.6.3-py3-none-any.whl", hash = "sha256:57f6f22bde4e042978bcd50176fdb381d7c21a9efa4041202288d3737a0c6a54"}, + {file = "setuptools-65.6.3.tar.gz", hash = "sha256:a7620757bf984b58deaf32fc8a4577a9bbc0850cf92c20e1ce41c38c19e5fb75"}, ] snowballstemmer = [ {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, @@ -755,6 +755,6 @@ tzdata = [ {file = "tzdata-2022.6.tar.gz", hash = "sha256:91f11db4503385928c15598c98573e3af07e7229181bee5375bd30f1695ddcae"}, ] urllib3 = [ - {file = "urllib3-1.26.12-py2.py3-none-any.whl", hash = "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"}, - {file = "urllib3-1.26.12.tar.gz", hash = "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e"}, + {file = "urllib3-1.26.13-py2.py3-none-any.whl", hash = "sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc"}, + {file = "urllib3-1.26.13.tar.gz", hash = "sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8"}, ] diff --git a/front/src/components/manage/users/InvitationsTable.vue b/front/src/components/manage/users/InvitationsTable.vue index 8c7098c1b..81d93d6a6 100644 --- a/front/src/components/manage/users/InvitationsTable.vue +++ b/front/src/components/manage/users/InvitationsTable.vue @@ -170,6 +170,11 @@ const labels = computed(() => ({ Owner +