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
	
	 Georg Krause
						Georg Krause