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, " | ||||
|         "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 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 utils as common_utils | ||||
| 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 utils as federation_utils | ||||
| from funkwhale_api.music import models as music_models | ||||
| from funkwhale_api.music import serializers as music_serializers | ||||
| from funkwhale_api.tags import models as tags_models | ||||
| from funkwhale_api.tags import serializers as tags_serializers | ||||
| from funkwhale_api.users import serializers as users_serializers | ||||
| 
 | ||||
| from . import categories | ||||
| from . import models | ||||
|  | @ -50,7 +54,10 @@ class ChannelMetadataSerializer(serializers.Serializer): | |||
| 
 | ||||
| class ChannelCreateSerializer(serializers.Serializer): | ||||
|     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) | ||||
|     tags = tags_serializers.TagsListField() | ||||
|     content_category = serializers.ChoiceField( | ||||
|  | @ -59,6 +66,11 @@ class ChannelCreateSerializer(serializers.Serializer): | |||
|     metadata = serializers.DictField(required=False) | ||||
| 
 | ||||
|     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) | ||||
|         metadata = validated_data.pop("metadata", {}) | ||||
|         if validated_data["content_category"] == "podcast": | ||||
|  | @ -68,6 +80,17 @@ class ChannelCreateSerializer(serializers.Serializer): | |||
|         validated_data["metadata"] = metadata | ||||
|         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 | ||||
|     def create(self, validated_data): | ||||
|         from . import views | ||||
|  |  | |||
|  | @ -138,6 +138,8 @@ class ChannelViewSet( | |||
|             "update", | ||||
|             "partial_update", | ||||
|         ] | ||||
|         if self.request.user.is_authenticated: | ||||
|             context["actor"] = self.request.user.actor | ||||
|         return context | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -23,7 +23,9 @@ def test_channel_serializer_create(factories): | |||
|         "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 | ||||
| 
 | ||||
|     channel = serializer.save(attributed_to=attributed_to) | ||||
|  | @ -49,6 +51,83 @@ def test_channel_serializer_create(factories): | |||
|     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): | ||||
|     attributed_to = factories["federation.Actor"](local=True) | ||||
| 
 | ||||
|  | @ -62,7 +141,9 @@ def test_channel_serializer_create_podcast(factories): | |||
|         "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 | ||||
| 
 | ||||
|     channel = serializer.save(attributed_to=attributed_to) | ||||
|  |  | |||
|  | @ -80,6 +80,7 @@ export default { | |||
|       let instanceLabel = this.$pgettext('Content/Admin/Menu','Instance information') | ||||
|       let usersLabel = this.$pgettext('*/*/*/Noun', 'Users') | ||||
|       let musicLabel = this.$pgettext('*/*/*/Noun', 'Music') | ||||
|       let channelsLabel = this.$pgettext('*/*/*', 'Channels') | ||||
|       let playlistsLabel = this.$pgettext('*/*/*', 'Playlists') | ||||
|       let federationLabel = this.$pgettext('*/*/*', 'Federation') | ||||
|       let moderationLabel = this.$pgettext('*/Moderation/*', 'Moderation') | ||||
|  | @ -120,6 +121,14 @@ export default { | |||
|             {name: "music__transcoding_cache_duration"}, | ||||
|           ] | ||||
|         }, | ||||
|         { | ||||
|           label: channelsLabel, | ||||
|           id: "channels", | ||||
|           settings: [ | ||||
|             {name: "audio__channels_enabled"}, | ||||
|             {name: "audio__max_channels"}, | ||||
|           ] | ||||
|         }, | ||||
|         { | ||||
|           label: playlistsLabel, | ||||
|           id: "playlists", | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 Eliot Berriot
						Eliot Berriot