Merge branch '170-api-stuff' into 'develop'
Max number of channels per user, duplicate username check See merge request funkwhale/funkwhale!1030
This commit is contained in:
		
						commit
						4bc11cc5d1
					
				|  | @ -14,3 +14,12 @@ class ChannelsEnabled(types.BooleanPreference): | ||||||
|         "If disabled, the channels feature will be completely switched off, " |         "If disabled, the channels feature will be completely switched off, " | ||||||
|         "and users won't be able to create channels or subscribe to them." |         "and users won't be able to create channels or subscribe to them." | ||||||
|     ) |     ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @global_preferences_registry.register | ||||||
|  | class MaxChannels(types.IntegerPreference): | ||||||
|  |     show_in_api = True | ||||||
|  |     section = audio | ||||||
|  |     default = 20 | ||||||
|  |     name = "max_channels" | ||||||
|  |     verbose_name = "Max channels allowed per user" | ||||||
|  |  | ||||||
|  | @ -1,3 +1,4 @@ | ||||||
|  | from django.conf import settings | ||||||
| from django.db import transaction | from django.db import transaction | ||||||
| 
 | 
 | ||||||
| from rest_framework import serializers | from rest_framework import serializers | ||||||
|  | @ -5,12 +6,15 @@ from rest_framework import serializers | ||||||
| from funkwhale_api.common import serializers as common_serializers | from funkwhale_api.common import serializers as common_serializers | ||||||
| from funkwhale_api.common import utils as common_utils | from funkwhale_api.common import utils as common_utils | ||||||
| from funkwhale_api.common import locales | from funkwhale_api.common import locales | ||||||
|  | from funkwhale_api.common import preferences | ||||||
|  | from funkwhale_api.federation import models as federation_models | ||||||
| from funkwhale_api.federation import serializers as federation_serializers | from funkwhale_api.federation import serializers as federation_serializers | ||||||
| from funkwhale_api.federation import utils as federation_utils | from funkwhale_api.federation import utils as federation_utils | ||||||
| from funkwhale_api.music import models as music_models | from funkwhale_api.music import models as music_models | ||||||
| from funkwhale_api.music import serializers as music_serializers | from funkwhale_api.music import serializers as music_serializers | ||||||
| from funkwhale_api.tags import models as tags_models | from funkwhale_api.tags import models as tags_models | ||||||
| from funkwhale_api.tags import serializers as tags_serializers | from funkwhale_api.tags import serializers as tags_serializers | ||||||
|  | from funkwhale_api.users import serializers as users_serializers | ||||||
| 
 | 
 | ||||||
| from . import categories | from . import categories | ||||||
| from . import models | from . import models | ||||||
|  | @ -50,7 +54,10 @@ class ChannelMetadataSerializer(serializers.Serializer): | ||||||
| 
 | 
 | ||||||
| class ChannelCreateSerializer(serializers.Serializer): | class ChannelCreateSerializer(serializers.Serializer): | ||||||
|     name = serializers.CharField(max_length=music_models.MAX_LENGTHS["ARTIST_NAME"]) |     name = serializers.CharField(max_length=music_models.MAX_LENGTHS["ARTIST_NAME"]) | ||||||
|     username = serializers.CharField(max_length=music_models.MAX_LENGTHS["ARTIST_NAME"]) |     username = serializers.CharField( | ||||||
|  |         max_length=music_models.MAX_LENGTHS["ARTIST_NAME"], | ||||||
|  |         validators=[users_serializers.ASCIIUsernameValidator()], | ||||||
|  |     ) | ||||||
|     description = common_serializers.ContentSerializer(allow_null=True) |     description = common_serializers.ContentSerializer(allow_null=True) | ||||||
|     tags = tags_serializers.TagsListField() |     tags = tags_serializers.TagsListField() | ||||||
|     content_category = serializers.ChoiceField( |     content_category = serializers.ChoiceField( | ||||||
|  | @ -59,6 +66,11 @@ class ChannelCreateSerializer(serializers.Serializer): | ||||||
|     metadata = serializers.DictField(required=False) |     metadata = serializers.DictField(required=False) | ||||||
| 
 | 
 | ||||||
|     def validate(self, validated_data): |     def validate(self, validated_data): | ||||||
|  |         existing_channels = self.context["actor"].owned_channels.count() | ||||||
|  |         if existing_channels >= preferences.get("audio__max_channels"): | ||||||
|  |             raise serializers.ValidationError( | ||||||
|  |                 "You have reached the maximum amount of allowed channels" | ||||||
|  |             ) | ||||||
|         validated_data = super().validate(validated_data) |         validated_data = super().validate(validated_data) | ||||||
|         metadata = validated_data.pop("metadata", {}) |         metadata = validated_data.pop("metadata", {}) | ||||||
|         if validated_data["content_category"] == "podcast": |         if validated_data["content_category"] == "podcast": | ||||||
|  | @ -68,6 +80,17 @@ class ChannelCreateSerializer(serializers.Serializer): | ||||||
|         validated_data["metadata"] = metadata |         validated_data["metadata"] = metadata | ||||||
|         return validated_data |         return validated_data | ||||||
| 
 | 
 | ||||||
|  |     def validate_username(self, value): | ||||||
|  |         if value.lower() in [n.lower() for n in settings.ACCOUNT_USERNAME_BLACKLIST]: | ||||||
|  |             raise serializers.ValidationError("This username is already taken") | ||||||
|  | 
 | ||||||
|  |         matching = federation_models.Actor.objects.local().filter( | ||||||
|  |             preferred_username__iexact=value | ||||||
|  |         ) | ||||||
|  |         if matching.exists(): | ||||||
|  |             raise serializers.ValidationError("This username is already taken") | ||||||
|  |         return value | ||||||
|  | 
 | ||||||
|     @transaction.atomic |     @transaction.atomic | ||||||
|     def create(self, validated_data): |     def create(self, validated_data): | ||||||
|         from . import views |         from . import views | ||||||
|  |  | ||||||
|  | @ -138,6 +138,8 @@ class ChannelViewSet( | ||||||
|             "update", |             "update", | ||||||
|             "partial_update", |             "partial_update", | ||||||
|         ] |         ] | ||||||
|  |         if self.request.user.is_authenticated: | ||||||
|  |             context["actor"] = self.request.user.actor | ||||||
|         return context |         return context | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -23,7 +23,9 @@ def test_channel_serializer_create(factories): | ||||||
|         "content_category": "other", |         "content_category": "other", | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     serializer = serializers.ChannelCreateSerializer(data=data) |     serializer = serializers.ChannelCreateSerializer( | ||||||
|  |         data=data, context={"actor": attributed_to} | ||||||
|  |     ) | ||||||
|     assert serializer.is_valid(raise_exception=True) is True |     assert serializer.is_valid(raise_exception=True) is True | ||||||
| 
 | 
 | ||||||
|     channel = serializer.save(attributed_to=attributed_to) |     channel = serializer.save(attributed_to=attributed_to) | ||||||
|  | @ -49,6 +51,83 @@ def test_channel_serializer_create(factories): | ||||||
|     assert channel.library.actor == attributed_to |     assert channel.library.actor == attributed_to | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | def test_channel_serializer_create_honor_max_channels_setting(factories, preferences): | ||||||
|  |     preferences["audio__max_channels"] = 1 | ||||||
|  |     attributed_to = factories["federation.Actor"](local=True) | ||||||
|  |     factories["audio.Channel"](attributed_to=attributed_to) | ||||||
|  |     data = { | ||||||
|  |         "name": "My channel", | ||||||
|  |         "username": "mychannel", | ||||||
|  |         "description": {"text": "This is my channel", "content_type": "text/markdown"}, | ||||||
|  |         "tags": ["hello", "world"], | ||||||
|  |         "content_category": "other", | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     serializer = serializers.ChannelCreateSerializer( | ||||||
|  |         data=data, context={"actor": attributed_to} | ||||||
|  |     ) | ||||||
|  |     with pytest.raises(serializers.serializers.ValidationError, match=r".*max.*"): | ||||||
|  |         assert serializer.is_valid(raise_exception=True) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def test_channel_serializer_create_validates_username_uniqueness(factories): | ||||||
|  |     attributed_to = factories["federation.Actor"](local=True) | ||||||
|  |     data = { | ||||||
|  |         "name": "My channel", | ||||||
|  |         "username": attributed_to.preferred_username.upper(), | ||||||
|  |         "description": {"text": "This is my channel", "content_type": "text/markdown"}, | ||||||
|  |         "tags": ["hello", "world"], | ||||||
|  |         "content_category": "other", | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     serializer = serializers.ChannelCreateSerializer( | ||||||
|  |         data=data, context={"actor": attributed_to} | ||||||
|  |     ) | ||||||
|  |     with pytest.raises( | ||||||
|  |         serializers.serializers.ValidationError, match=r".*username is already taken.*" | ||||||
|  |     ): | ||||||
|  |         assert serializer.is_valid(raise_exception=True) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def test_channel_serializer_create_validates_username_chars(factories): | ||||||
|  |     attributed_to = factories["federation.Actor"](local=True) | ||||||
|  |     data = { | ||||||
|  |         "name": "My channel", | ||||||
|  |         "username": "hello world", | ||||||
|  |         "description": {"text": "This is my channel", "content_type": "text/markdown"}, | ||||||
|  |         "tags": ["hello", "world"], | ||||||
|  |         "content_category": "other", | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     serializer = serializers.ChannelCreateSerializer( | ||||||
|  |         data=data, context={"actor": attributed_to} | ||||||
|  |     ) | ||||||
|  |     with pytest.raises( | ||||||
|  |         serializers.serializers.ValidationError, match=r".*Enter a valid username.*" | ||||||
|  |     ): | ||||||
|  |         assert serializer.is_valid(raise_exception=True) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def test_channel_serializer_create_validates_blacklisted_username(factories, settings): | ||||||
|  |     settings.ACCOUNT_USERNAME_BLACKLIST = ["forBidden"] | ||||||
|  |     attributed_to = factories["federation.Actor"](local=True) | ||||||
|  |     data = { | ||||||
|  |         "name": "My channel", | ||||||
|  |         "username": "FORBIDDEN", | ||||||
|  |         "description": {"text": "This is my channel", "content_type": "text/markdown"}, | ||||||
|  |         "tags": ["hello", "world"], | ||||||
|  |         "content_category": "other", | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     serializer = serializers.ChannelCreateSerializer( | ||||||
|  |         data=data, context={"actor": attributed_to} | ||||||
|  |     ) | ||||||
|  |     with pytest.raises( | ||||||
|  |         serializers.serializers.ValidationError, match=r".*username is already taken.*" | ||||||
|  |     ): | ||||||
|  |         assert serializer.is_valid(raise_exception=True) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| def test_channel_serializer_create_podcast(factories): | def test_channel_serializer_create_podcast(factories): | ||||||
|     attributed_to = factories["federation.Actor"](local=True) |     attributed_to = factories["federation.Actor"](local=True) | ||||||
| 
 | 
 | ||||||
|  | @ -62,7 +141,9 @@ def test_channel_serializer_create_podcast(factories): | ||||||
|         "metadata": {"itunes_category": "Sports", "language": "en"}, |         "metadata": {"itunes_category": "Sports", "language": "en"}, | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     serializer = serializers.ChannelCreateSerializer(data=data) |     serializer = serializers.ChannelCreateSerializer( | ||||||
|  |         data=data, context={"actor": attributed_to} | ||||||
|  |     ) | ||||||
|     assert serializer.is_valid(raise_exception=True) is True |     assert serializer.is_valid(raise_exception=True) is True | ||||||
| 
 | 
 | ||||||
|     channel = serializer.save(attributed_to=attributed_to) |     channel = serializer.save(attributed_to=attributed_to) | ||||||
|  |  | ||||||
|  | @ -80,6 +80,7 @@ export default { | ||||||
|       let instanceLabel = this.$pgettext('Content/Admin/Menu','Instance information') |       let instanceLabel = this.$pgettext('Content/Admin/Menu','Instance information') | ||||||
|       let usersLabel = this.$pgettext('*/*/*/Noun', 'Users') |       let usersLabel = this.$pgettext('*/*/*/Noun', 'Users') | ||||||
|       let musicLabel = this.$pgettext('*/*/*/Noun', 'Music') |       let musicLabel = this.$pgettext('*/*/*/Noun', 'Music') | ||||||
|  |       let channelsLabel = this.$pgettext('*/*/*', 'Channels') | ||||||
|       let playlistsLabel = this.$pgettext('*/*/*', 'Playlists') |       let playlistsLabel = this.$pgettext('*/*/*', 'Playlists') | ||||||
|       let federationLabel = this.$pgettext('*/*/*', 'Federation') |       let federationLabel = this.$pgettext('*/*/*', 'Federation') | ||||||
|       let moderationLabel = this.$pgettext('*/Moderation/*', 'Moderation') |       let moderationLabel = this.$pgettext('*/Moderation/*', 'Moderation') | ||||||
|  | @ -120,6 +121,14 @@ export default { | ||||||
|             {name: "music__transcoding_cache_duration"}, |             {name: "music__transcoding_cache_duration"}, | ||||||
|           ] |           ] | ||||||
|         }, |         }, | ||||||
|  |         { | ||||||
|  |           label: channelsLabel, | ||||||
|  |           id: "channels", | ||||||
|  |           settings: [ | ||||||
|  |             {name: "audio__channels_enabled"}, | ||||||
|  |             {name: "audio__max_channels"}, | ||||||
|  |           ] | ||||||
|  |         }, | ||||||
|         { |         { | ||||||
|           label: playlistsLabel, |           label: playlistsLabel, | ||||||
|           id: "playlists", |           id: "playlists", | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue
	
	 Eliot Berriot
						Eliot Berriot