First round of improvements to channel management:

- use modals
- less proeminent button
- field styling/labels
This commit is contained in:
Eliot Berriot 2020-02-23 15:31:03 +01:00
commit e59cc33378
103 changed files with 3201 additions and 447 deletions

View file

@ -6,6 +6,8 @@ from django.core.serializers.json import DjangoJSONEncoder
from django.db import models
from django.urls import reverse
from django.utils import timezone
from django.db.models.signals import post_delete
from django.dispatch import receiver
from funkwhale_api.federation import keys
from funkwhale_api.federation import models as federation_models
@ -44,14 +46,22 @@ class Channel(models.Model):
)
def get_absolute_url(self):
return federation_utils.full_url("/channels/{}".format(self.uuid))
suffix = self.uuid
if self.actor.is_local:
suffix = self.actor.preferred_username
else:
suffix = self.actor.full_username
return federation_utils.full_url("/channels/{}".format(suffix))
def get_rss_url(self):
if not self.artist.is_local:
return self.rss_url
return federation_utils.full_url(
reverse("api:v1:channels-rss", kwargs={"uuid": self.uuid})
reverse(
"api:v1:channels-rss",
kwargs={"composite": self.actor.preferred_username},
)
)
@ -62,3 +72,10 @@ def generate_actor(username, **kwargs):
actor_data["public_key"] = public.decode("utf-8")
return federation_models.Actor.objects.create(**actor_data)
@receiver(post_delete, sender=Channel)
def delete_channel_related_objs(instance, **kwargs):
instance.library.delete()
instance.actor.delete()
instance.artist.delete()

View file

@ -3,6 +3,8 @@ from django.db import transaction
from rest_framework import serializers
from django.contrib.staticfiles.templatetags.staticfiles import static
from funkwhale_api.common import serializers as common_serializers
from funkwhale_api.common import utils as common_utils
from funkwhale_api.common import locales
@ -24,7 +26,7 @@ class ChannelMetadataSerializer(serializers.Serializer):
itunes_category = serializers.ChoiceField(
choices=categories.ITUNES_CATEGORIES, required=True
)
itunes_subcategory = serializers.CharField(required=False)
itunes_subcategory = serializers.CharField(required=False, allow_null=True)
language = serializers.ChoiceField(required=True, choices=locales.ISO_639_CHOICES)
copyright = serializers.CharField(required=False, allow_null=True, max_length=255)
owner_name = serializers.CharField(required=False, allow_null=True, max_length=255)
@ -64,6 +66,7 @@ class ChannelCreateSerializer(serializers.Serializer):
choices=music_models.ARTIST_CONTENT_CATEGORY_CHOICES
)
metadata = serializers.DictField(required=False)
cover = music_serializers.COVER_WRITE_FIELD
def validate(self, validated_data):
existing_channels = self.context["actor"].owned_channels.count()
@ -95,15 +98,15 @@ class ChannelCreateSerializer(serializers.Serializer):
def create(self, validated_data):
from . import views
cover = validated_data.pop("cover", None)
description = validated_data.get("description")
artist = music_models.Artist.objects.create(
attributed_to=validated_data["attributed_to"],
name=validated_data["name"],
content_category=validated_data["content_category"],
attachment_cover=cover,
)
description_obj = common_utils.attach_content(
artist, "description", description
)
common_utils.attach_content(artist, "description", description)
if validated_data.get("tags", []):
tags_models.set_tags(artist, *validated_data["tags"])
@ -113,9 +116,8 @@ class ChannelCreateSerializer(serializers.Serializer):
attributed_to=validated_data["attributed_to"],
metadata=validated_data["metadata"],
)
summary = description_obj.rendered if description_obj else None
channel.actor = models.generate_actor(
validated_data["username"], summary=summary, name=validated_data["name"],
validated_data["username"], name=validated_data["name"],
)
channel.library = music_models.Library.objects.create(
@ -142,6 +144,7 @@ class ChannelUpdateSerializer(serializers.Serializer):
choices=music_models.ARTIST_CONTENT_CATEGORY_CHOICES
)
metadata = serializers.DictField(required=False)
cover = music_serializers.COVER_WRITE_FIELD
def validate(self, validated_data):
validated_data = super().validate(validated_data)
@ -194,6 +197,9 @@ class ChannelUpdateSerializer(serializers.Serializer):
("content_category", validated_data["content_category"])
)
if "cover" in validated_data:
artist_update_fields.append(("attachment_cover", validated_data["cover"]))
if actor_update_fields:
for field, value in actor_update_fields:
setattr(obj.actor, field, value)
@ -292,7 +298,7 @@ def rss_serialize_item(upload):
# we enforce MP3, since it's the only format supported everywhere
"url": federation_utils.full_url(upload.get_listen_url(to="mp3")),
"length": upload.size or 0,
"type": upload.mimetype or "audio/mpeg",
"type": "audio/mpeg",
}
],
}
@ -362,6 +368,11 @@ def rss_serialize_channel(channel):
data["itunes:image"] = [
{"href": channel.artist.attachment_cover.download_url_original}
]
else:
placeholder_url = federation_utils.full_url(
static("images/podcasts-cover-placeholder.png")
)
data["itunes:image"] = [{"href": placeholder_url}]
tagged_items = getattr(channel.artist, "_prefetched_tagged_items", [])

View file

@ -1,26 +1,33 @@
import urllib.parse
from django.conf import settings
from django.db.models import Q
from django.urls import reverse
from rest_framework import serializers
from funkwhale_api.common import preferences
from funkwhale_api.common import utils
from funkwhale_api.federation import utils as federation_utils
from funkwhale_api.music import spa_views
from . import models
def channel_detail(request, uuid):
queryset = models.Channel.objects.filter(uuid=uuid).select_related(
def channel_detail(query):
queryset = models.Channel.objects.filter(query).select_related(
"artist__attachment_cover", "actor", "library"
)
try:
obj = queryset.get()
except models.Channel.DoesNotExist:
return []
obj_url = utils.join_url(
settings.FUNKWHALE_URL,
utils.spa_reverse("channel_detail", kwargs={"uuid": obj.uuid}),
utils.spa_reverse(
"channel_detail", kwargs={"username": obj.actor.full_username}
),
)
metas = [
{"tag": "meta", "property": "og:url", "content": obj_url},
@ -72,3 +79,25 @@ def channel_detail(request, uuid):
# twitter player is also supported in various software
metas += spa_views.get_twitter_card_metas(type="channel", id=obj.uuid)
return metas
def channel_detail_uuid(request, uuid):
validator = serializers.UUIDField().to_internal_value
try:
uuid = validator(uuid)
except serializers.ValidationError:
return []
return channel_detail(Q(uuid=uuid))
def channel_detail_username(request, username):
validator = federation_utils.get_actor_data_from_username
try:
username_data = validator(username)
except serializers.ValidationError:
return []
query = Q(
actor__domain=username_data["domain"],
actor__preferred_username__iexact=username_data["username"],
)
return channel_detail(query)

View file

@ -7,18 +7,21 @@ from rest_framework import viewsets
from django import http
from django.db import transaction
from django.db.models import Count, Prefetch
from django.db.models import Count, Prefetch, Q
from django.db.utils import IntegrityError
from funkwhale_api.common import locales
from funkwhale_api.common import permissions
from funkwhale_api.common import preferences
from funkwhale_api.common.mixins import MultipleLookupDetailMixin
from funkwhale_api.federation import models as federation_models
from funkwhale_api.federation import routes
from funkwhale_api.federation import utils as federation_utils
from funkwhale_api.music import models as music_models
from funkwhale_api.music import views as music_views
from funkwhale_api.users.oauth import permissions as oauth_permissions
from . import filters, models, renderers, serializers
from . import categories, filters, models, renderers, serializers
ARTIST_PREFETCH_QS = (
music_models.Artist.objects.select_related("description", "attachment_cover",)
@ -36,6 +39,7 @@ class ChannelsMixin(object):
class ChannelViewSet(
ChannelsMixin,
MultipleLookupDetailMixin,
mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
@ -43,7 +47,20 @@ class ChannelViewSet(
mixins.DestroyModelMixin,
viewsets.GenericViewSet,
):
lookup_field = "uuid"
url_lookups = [
{
"lookup_field": "uuid",
"validator": serializers.serializers.UUIDField().to_internal_value,
},
{
"lookup_field": "username",
"validator": federation_utils.get_actor_data_from_username,
"get_query": lambda v: Q(
actor__domain=v["domain"],
actor__preferred_username__iexact=v["username"],
),
},
]
filterset_class = filters.ChannelFilter
serializer_class = serializers.ChannelSerializer
queryset = (
@ -134,6 +151,25 @@ class ChannelViewSet(
data = serializers.rss_serialize_channel_full(channel=object, uploads=uploads)
return response.Response(data, status=200)
@decorators.action(
methods=["get"],
detail=False,
url_path="metadata-choices",
url_name="metadata_choices",
permission_classes=[],
)
def metedata_choices(self, request, *args, **kwargs):
data = {
"language": [
{"value": code, "label": name} for code, name in locales.ISO_639_CHOICES
],
"itunes_category": [
{"value": code, "label": code, "children": children}
for code, children in categories.ITUNES_CATEGORIES.items()
],
}
return response.Response(data)
def get_serializer_context(self):
context = super().get_serializer_context()
context["subscriptions_count"] = self.action in [
@ -152,7 +188,7 @@ class ChannelViewSet(
{"type": "Delete", "object": {"type": instance.actor.type}},
context={"actor": instance.actor},
)
instance.delete()
instance.__class__.objects.filter(pk=instance.pk).delete()
class SubscriptionsViewSet(