Merge branch 'stable' into develop
This commit is contained in:
commit
0602de6d81
42
CHANGELOG
42
CHANGELOG
|
@ -10,6 +10,48 @@ This changelog is viewable on the web at https://docs.funkwhale.audio/changelog.
|
||||||
|
|
||||||
.. towncrier
|
.. 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)
|
1.2.8 (2022-09-12)
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
__version__ = "1.2.8"
|
__version__ = "1.2.9"
|
||||||
__version_info__ = tuple(
|
__version_info__ = tuple(
|
||||||
[
|
[
|
||||||
int(num) if num.isdigit() else num
|
int(num) if num.isdigit() else num
|
||||||
|
|
|
@ -126,6 +126,7 @@ def get_spa_file(spa_url, name):
|
||||||
utils.join_url(spa_url, name),
|
utils.join_url(spa_url, name),
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
response.encoding = "utf-8"
|
||||||
content = response.text
|
content = response.text
|
||||||
caches["local"].set(cache_key, content, settings.FUNKWHALE_SPA_HTML_CACHE_DURATION)
|
caches["local"].set(cache_key, content, settings.FUNKWHALE_SPA_HTML_CACHE_DURATION)
|
||||||
return content
|
return content
|
||||||
|
|
|
@ -30,8 +30,11 @@ def verify_date(raw_date):
|
||||||
delta = datetime.timedelta(seconds=DATE_HEADER_VALID_FOR)
|
delta = datetime.timedelta(seconds=DATE_HEADER_VALID_FOR)
|
||||||
now = timezone.now()
|
now = timezone.now()
|
||||||
if dt < now - delta or dt > now + delta:
|
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(
|
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
|
return dt
|
||||||
|
|
|
@ -98,12 +98,28 @@ class ManageUserSerializer(serializers.ModelSerializer):
|
||||||
class ManageInvitationSerializer(serializers.ModelSerializer):
|
class ManageInvitationSerializer(serializers.ModelSerializer):
|
||||||
users = ManageUserSimpleSerializer(many=True, required=False)
|
users = ManageUserSimpleSerializer(many=True, required=False)
|
||||||
owner = ManageUserSimpleSerializer(required=False)
|
owner = ManageUserSimpleSerializer(required=False)
|
||||||
|
invited_user = ManageUserSimpleSerializer(required=False)
|
||||||
code = serializers.CharField(required=False, allow_null=True)
|
code = serializers.CharField(required=False, allow_null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = users_models.Invitation
|
model = users_models.Invitation
|
||||||
fields = ("id", "owner", "code", "expiration_date", "creation_date", "users")
|
fields = (
|
||||||
read_only_fields = ["id", "expiration_date", "owner", "creation_date", "users"]
|
"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):
|
def validate_code(self, value):
|
||||||
if not value:
|
if not value:
|
||||||
|
|
|
@ -21,7 +21,7 @@ class Command(BaseCommand):
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
skipped = models.Uploads.objects.filter(import_status="skipped")
|
skipped = models.Upload.objects.filter(import_status="skipped")
|
||||||
count = len(skipped)
|
count = len(skipped)
|
||||||
if options["force"]:
|
if options["force"]:
|
||||||
skipped.delete()
|
skipped.delete()
|
||||||
|
|
|
@ -668,8 +668,12 @@ class UploadQuerySet(common_models.NullsLastQuerySet):
|
||||||
libraries = Library.objects.viewable_by(actor)
|
libraries = Library.objects.viewable_by(actor)
|
||||||
|
|
||||||
if include:
|
if include:
|
||||||
return self.filter(library__in=libraries, import_status="finished")
|
return self.filter(
|
||||||
return self.exclude(library__in=libraries, import_status="finished")
|
library__in=libraries, import_status__in=["finished", "skipped"]
|
||||||
|
)
|
||||||
|
return self.exclude(
|
||||||
|
library__in=libraries, import_status__in=["finished", "skipped"]
|
||||||
|
)
|
||||||
|
|
||||||
def local(self, include=True):
|
def local(self, include=True):
|
||||||
query = models.Q(library__actor__domain_id=settings.FEDERATION_HOSTNAME)
|
query = models.Q(library__actor__domain_id=settings.FEDERATION_HOSTNAME)
|
||||||
|
|
|
@ -58,6 +58,9 @@ class InvitationFactory(NoUpdateOnCreate, factory.django.DjangoModelFactory):
|
||||||
|
|
||||||
class Params:
|
class Params:
|
||||||
expired = factory.Trait(expiration_date=factory.LazyFunction(timezone.now))
|
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):
|
class PasswordSetter(factory.PostGenerationMethodCall):
|
||||||
|
|
|
@ -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),
|
||||||
|
]
|
|
@ -335,6 +335,9 @@ class Invitation(models.Model):
|
||||||
owner = models.ForeignKey(
|
owner = models.ForeignKey(
|
||||||
User, related_name="invitations", on_delete=models.CASCADE
|
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)
|
code = models.CharField(max_length=50, unique=True)
|
||||||
|
|
||||||
objects = InvitationQuerySet.as_manager()
|
objects = InvitationQuerySet.as_manager()
|
||||||
|
@ -349,6 +352,10 @@ class Invitation(models.Model):
|
||||||
|
|
||||||
return super().save(**kwargs)
|
return super().save(**kwargs)
|
||||||
|
|
||||||
|
def set_invited_user(self, user):
|
||||||
|
self.invited_user = user
|
||||||
|
super().save()
|
||||||
|
|
||||||
|
|
||||||
class Application(oauth2_models.AbstractApplication):
|
class Application(oauth2_models.AbstractApplication):
|
||||||
scope = models.TextField(blank=True)
|
scope = models.TextField(blank=True)
|
||||||
|
|
|
@ -38,6 +38,9 @@ class RegisterView(registration_views.RegisterView):
|
||||||
if not user.is_active:
|
if not user.is_active:
|
||||||
# manual approval, we need to send the confirmation e-mail by hand
|
# manual approval, we need to send the confirmation e-mail by hand
|
||||||
authentication.send_email_confirmation(self.request, user)
|
authentication.send_email_confirmation(self.request, user)
|
||||||
|
if user.invitation:
|
||||||
|
user.invitation.set_invited_user(user)
|
||||||
|
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -211,7 +211,8 @@ def test_library(factories):
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@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):
|
def test_playable_by_correct_status(status, expected, factories):
|
||||||
upload = factories["music.Upload"](
|
upload = factories["music.Upload"](
|
||||||
|
|
|
@ -105,6 +105,13 @@ def test_invitation_generates_random_code_on_save(factories):
|
||||||
assert len(invitation.code) >= 6
|
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):
|
def test_invitation_expires_after_delay(factories, settings):
|
||||||
delay = settings.USERS_INVITATION_EXPIRATION_DAYS
|
delay = settings.USERS_INVITATION_EXPIRATION_DAYS
|
||||||
invitation = factories["users.Invitation"]()
|
invitation = factories["users.Invitation"]()
|
||||||
|
|
|
@ -279,7 +279,7 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "setuptools"
|
name = "setuptools"
|
||||||
version = "65.6.0"
|
version = "65.6.3"
|
||||||
description = "Easily download, build, install, upgrade, and uninstall Python packages"
|
description = "Easily download, build, install, upgrade, and uninstall Python packages"
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
|
@ -498,11 +498,11 @@ python-versions = ">=2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "urllib3"
|
name = "urllib3"
|
||||||
version = "1.26.12"
|
version = "1.26.13"
|
||||||
description = "HTTP library with thread-safe connection pooling, file post, and more."
|
description = "HTTP library with thread-safe connection pooling, file post, and more."
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
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]
|
[package.extras]
|
||||||
brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"]
|
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"},
|
{file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"},
|
||||||
]
|
]
|
||||||
setuptools = [
|
setuptools = [
|
||||||
{file = "setuptools-65.6.0-py3-none-any.whl", hash = "sha256:6211d2f5eddad8757bd0484923ca7c0a6302ebc4ab32ea5e94357176e0ca0840"},
|
{file = "setuptools-65.6.3-py3-none-any.whl", hash = "sha256:57f6f22bde4e042978bcd50176fdb381d7c21a9efa4041202288d3737a0c6a54"},
|
||||||
{file = "setuptools-65.6.0.tar.gz", hash = "sha256:d1eebf881c6114e51df1664bc2c9133d022f78d12d5f4f665b9191f084e2862d"},
|
{file = "setuptools-65.6.3.tar.gz", hash = "sha256:a7620757bf984b58deaf32fc8a4577a9bbc0850cf92c20e1ce41c38c19e5fb75"},
|
||||||
]
|
]
|
||||||
snowballstemmer = [
|
snowballstemmer = [
|
||||||
{file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"},
|
{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"},
|
{file = "tzdata-2022.6.tar.gz", hash = "sha256:91f11db4503385928c15598c98573e3af07e7229181bee5375bd30f1695ddcae"},
|
||||||
]
|
]
|
||||||
urllib3 = [
|
urllib3 = [
|
||||||
{file = "urllib3-1.26.12-py2.py3-none-any.whl", hash = "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"},
|
{file = "urllib3-1.26.13-py2.py3-none-any.whl", hash = "sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc"},
|
||||||
{file = "urllib3-1.26.12.tar.gz", hash = "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e"},
|
{file = "urllib3-1.26.13.tar.gz", hash = "sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8"},
|
||||||
]
|
]
|
||||||
|
|
|
@ -170,6 +170,11 @@ const labels = computed(() => ({
|
||||||
Owner
|
Owner
|
||||||
</translate>
|
</translate>
|
||||||
</th>
|
</th>
|
||||||
|
<th>
|
||||||
|
<translate translate-context="Content/Admin/Table.Label/Noun">
|
||||||
|
User
|
||||||
|
</translate>
|
||||||
|
</th>
|
||||||
<th>
|
<th>
|
||||||
<translate translate-context="*/*/*">
|
<translate translate-context="*/*/*">
|
||||||
Status
|
Status
|
||||||
|
@ -199,6 +204,11 @@ const labels = computed(() => ({
|
||||||
{{ scope.obj.owner.username }}
|
{{ scope.obj.owner.username }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
<span v-if="scope.obj.invited_user">
|
||||||
|
{{ scope.obj.invited_user.username }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span
|
<span
|
||||||
v-if="scope.obj.users.length > 0"
|
v-if="scope.obj.users.length > 0"
|
||||||
|
|
|
@ -1299,9 +1299,9 @@
|
||||||
"@types/lodash" "*"
|
"@types/lodash" "*"
|
||||||
|
|
||||||
"@types/lodash@*":
|
"@types/lodash@*":
|
||||||
version "4.14.189"
|
version "4.14.190"
|
||||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.189.tgz#975ff8c38da5ae58b751127b19ad5e44b5b7f6d2"
|
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.190.tgz#d8e99647af141c63902d0ca53cf2b34d2df33545"
|
||||||
integrity sha512-kb9/98N6X8gyME9Cf7YaqIMvYGnBSWqEci6tiettE6iJWH1XdJz/PO8LB0GtLCG7x8dU3KWhZT+lA1a35127tA==
|
integrity sha512-5iJ3FBJBvQHQ8sFhEhJfjUP+G+LalhavTkYyrAYqz5MEJG+erSv0k9KJLb6q7++17Lafk1scaTIFXcMJlwK8Mw==
|
||||||
|
|
||||||
"@types/minimatch@*":
|
"@types/minimatch@*":
|
||||||
version "5.1.2"
|
version "5.1.2"
|
||||||
|
@ -5053,9 +5053,9 @@ tempy@^0.6.0:
|
||||||
unique-string "^2.0.0"
|
unique-string "^2.0.0"
|
||||||
|
|
||||||
terser@^5.0.0:
|
terser@^5.0.0:
|
||||||
version "5.15.1"
|
version "5.16.0"
|
||||||
resolved "https://registry.yarnpkg.com/terser/-/terser-5.15.1.tgz#8561af6e0fd6d839669c73b92bdd5777d870ed6c"
|
resolved "https://registry.yarnpkg.com/terser/-/terser-5.16.0.tgz#29362c6f5506e71545c73b069ccd199bb28f7f54"
|
||||||
integrity sha512-K1faMUvpm/FBxjBXud0LWVAGxmvoPbZbfTCYbSgaaYQaIXI3/TdI7a7ZGA73Zrou6Q8Zmz3oeUTsp/dj+ag2Xw==
|
integrity sha512-KjTV81QKStSfwbNiwlBXfcgMcOloyuRdb62/iLFPGBcVNF4EXjhdYBhYHmbJpiBrVxZhDvltE11j+LBQUxEEJg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@jridgewell/source-map" "^0.3.2"
|
"@jridgewell/source-map" "^0.3.2"
|
||||||
acorn "^8.5.0"
|
acorn "^8.5.0"
|
||||||
|
|
Loading…
Reference in New Issue