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