Compare commits

..

2 Commits

8 changed files with 414 additions and 46 deletions

View File

@ -1,5 +1,7 @@
from rest_framework import serializers
from akarpov.utils.models import get_model_user_field
class RecursiveField(serializers.Serializer):
def to_representation(self, value):
@ -9,6 +11,19 @@ def to_representation(self, value):
class SetUserModelSerializer(serializers.ModelSerializer):
def create(self, validated_data):
creator = self.context["request"].user
obj = self.Meta.model.objects.create(creator=creator, **validated_data)
if self.context["request"].user.is_authenticated:
creator = self.context["request"].user
else:
creator = None
validated_data[
get_model_user_field(
self.Meta.model._meta.app_label, self.Meta.model._meta.model_name
)
] = creator
try:
obj = self.Meta.model.objects.create(**validated_data)
except TypeError:
raise serializers.ValidationError(
{"detail": "You need to login to create this object"}
)
return obj

View File

@ -0,0 +1,9 @@
from django_filters import rest_framework as filters
from akarpov.music.models import Playlist
class PlaylistFilter(filters.FilterSet):
class Meta:
model = Playlist
fields = ()

View File

@ -2,37 +2,40 @@
from rest_framework import serializers
from akarpov.common.api.serializers import SetUserModelSerializer
from akarpov.music.models import Album, Author, Playlist, Song
from akarpov.music.models import (
Album,
Author,
Playlist,
PlaylistSong,
Song,
SongUserRating,
)
from akarpov.users.api.serializers import UserPublicInfoSerializer
class AuthorSerializer(serializers.ModelSerializer):
url = serializers.SerializerMethodField(method_name="get_url")
@extend_schema_field(serializers.URLField)
def get_url(self, obj):
return obj.get_absolute_url()
class Meta:
model = Author
fields = ["name", "link", "image_cropped", "url"]
fields = ["name", "slug", "link", "image_cropped"]
class AlbumSerializer(serializers.ModelSerializer):
url = serializers.SerializerMethodField(method_name="get_url")
@extend_schema_field(serializers.URLField)
def get_url(self, obj):
return obj.get_absolute_url()
class Meta:
model = Album
fields = ["name", "link", "image_cropped", "url"]
fields = ["name", "slug", "link", "image_cropped"]
class SongSerializer(serializers.ModelSerializer):
authors = AuthorSerializer(many=True)
album = AlbumSerializer()
liked = serializers.SerializerMethodField(method_name="get_liked")
@extend_schema_field(serializers.BooleanField)
def get_liked(self, obj):
if self.context["request"].user.is_authenticated:
return SongUserRating.objects.filter(
song=obj, user=self.context["request"].user, like=True
).exists()
class Meta:
model = Song
@ -45,6 +48,7 @@ class Meta:
"file",
"authors",
"album",
"liked",
]
extra_kwargs = {
"slug": {"read_only": True},
@ -58,12 +62,15 @@ class ListSongSerializer(SetUserModelSerializer):
album = serializers.CharField(source="album.name", read_only=True)
liked = serializers.SerializerMethodField(method_name="get_liked")
@extend_schema_field(serializers.BooleanField)
def get_liked(self, obj):
if "likes" in self.context:
return self.context["likes"]
return obj.id in self.context["likes_ids"]
class Meta:
model = Song
fields = ["name", "slug", "file", "image_cropped", "length", "album"]
fields = ["name", "slug", "file", "image_cropped", "length", "album", "liked"]
extra_kwargs = {
"slug": {"read_only": True},
"image_cropped": {"read_only": True},
@ -73,14 +80,15 @@ class Meta:
class PlaylistSerializer(SetUserModelSerializer):
creator = UserPublicInfoSerializer()
creator = UserPublicInfoSerializer(read_only=True)
class Meta:
model = Playlist
fields = ["name", "slug", "private", "creator"]
fields = ["name", "length", "slug", "private", "creator"]
extra_kwargs = {
"slug": {"read_only": True},
"creator": {"read_only": True},
"length": {"read_only": True},
}
@ -95,3 +103,131 @@ class Meta:
"slug": {"read_only": True},
"creator": {"read_only": True},
}
class AddSongToPlaylistSerializer(serializers.ModelSerializer):
song = serializers.SlugField()
playlist = serializers.SlugField()
class Meta:
model = Playlist
fields = ["song", "playlist"]
extra_kwargs = {
"song": {"write_only": True},
"playlist": {"write_only": True},
}
def validate(self, attrs):
if not Playlist.objects.filter(slug=attrs["playlist"]).exists():
raise serializers.ValidationError("Playlist not found")
if not Song.objects.filter(slug=attrs["song"]).exists():
raise serializers.ValidationError("Song not found")
if self.context["create"]:
if PlaylistSong.objects.filter(
playlist__slug=attrs["playlist"], song__slug=attrs["song"]
).exists():
raise serializers.ValidationError("Song already in playlist")
else:
if not PlaylistSong.objects.filter(
playlist__slug=attrs["playlist"], song__slug=attrs["song"]
).exists():
raise serializers.ValidationError("Song not in playlist")
return attrs
def create(self, validated_data):
playlist = Playlist.objects.get(slug=validated_data["playlist"])
song = Song.objects.get(slug=validated_data["song"])
if not self.context["create"]:
playlist_song = PlaylistSong.objects.get(
playlist=playlist, song=song
).delete()
else:
order = playlist.songs.count()
playlist_song = playlist.songs.create(song=song, order=order)
return playlist_song
class LikeDislikeSongSerializer(serializers.ModelSerializer):
song = serializers.SlugField()
class Meta:
model = SongUserRating
fields = ["song"]
extra_kwargs = {
"song": {"write_only": True},
}
def validate(self, attrs):
if not Song.objects.filter(slug=attrs["song"]).exists():
raise serializers.ValidationError("Song not found")
return attrs
def create(self, validated_data):
song = Song.objects.get(slug=validated_data["song"])
if self.context["like"]:
if SongUserRating.objects.filter(
song=song, user=self.context["request"].user
).exists():
song_user_rating = SongUserRating.objects.get(
song=song, user=self.context["request"].user
)
if song_user_rating.like:
song_user_rating.delete()
else:
song_user_rating.like = True
song_user_rating.save()
else:
song_user_rating = SongUserRating.objects.create(
song=song, user=self.context["request"].user, like=True
)
else:
if SongUserRating.objects.filter(
song=song, user=self.context["request"].user
).exists():
song_user_rating = SongUserRating.objects.get(
song=song, user=self.context["request"].user
)
if not song_user_rating.like:
song_user_rating.delete()
else:
song_user_rating.like = False
song_user_rating.save()
else:
song_user_rating = SongUserRating.objects.create(
song=song, user=self.context["request"].user, like=False
)
return song_user_rating
class ListPlaylistSerializer(serializers.ModelSerializer):
class Meta:
model = Playlist
fields = ["name", "slug", "private"]
extra_kwargs = {
"slug": {"read_only": True},
"private": {"read_only": True},
}
class FullAlbumSerializer(serializers.ModelSerializer):
songs = ListSongSerializer(many=True, read_only=True)
class Meta:
model = Album
fields = ["name", "link", "image", "songs"]
extra_kwargs = {
"link": {"read_only": True},
"image": {"read_only": True},
}
class FullAuthorSerializer(serializers.ModelSerializer):
songs = ListSongSerializer(many=True, read_only=True)
class Meta:
model = Author
fields = ["name", "link", "image", "songs"]
extra_kwargs = {
"link": {"read_only": True},
"image": {"read_only": True},
}

View File

@ -1,8 +1,19 @@
from django.urls import path
from akarpov.music.api.views import (
AddSongToPlaylistAPIView,
DislikeSongAPIView,
LikeSongAPIView,
ListAlbumsAPIView,
ListAuthorsAPIView,
ListCreatePlaylistAPIView,
ListCreateSongAPIView,
ListDislikedSongsAPIView,
ListLikedSongsAPIView,
ListPublicPlaylistAPIView,
RemoveSongFromPlaylistAPIView,
RetrieveUpdateDestroyAlbumAPIView,
RetrieveUpdateDestroyAuthorAPIView,
RetrieveUpdateDestroyPlaylistAPIView,
RetrieveUpdateDestroySongAPIView,
)
@ -10,9 +21,16 @@
app_name = "music"
urlpatterns = [
path("song/liked/", ListLikedSongsAPIView.as_view(), name="list_liked"),
path("song/disliked/", ListDislikedSongsAPIView.as_view(), name="list_disliked"),
path(
"playlists/", ListCreatePlaylistAPIView.as_view(), name="list_create_playlist"
),
path(
"playlists/public/",
ListPublicPlaylistAPIView.as_view(),
name="list_public",
),
path(
"playlists/<str:slug>",
RetrieveUpdateDestroyPlaylistAPIView.as_view(),
@ -24,4 +42,20 @@
RetrieveUpdateDestroySongAPIView.as_view(),
name="retrieve_update_delete_song",
),
path("song/like/", LikeSongAPIView.as_view()),
path("song/dislike/", DislikeSongAPIView.as_view()),
path("playlists/add/", AddSongToPlaylistAPIView.as_view()),
path("playlists/remove/", RemoveSongFromPlaylistAPIView.as_view()),
path("albums/", ListAlbumsAPIView.as_view(), name="list_albums"),
path(
"albums/<str:slug>",
RetrieveUpdateDestroyAlbumAPIView.as_view(),
name="retrieve_update_delete_album",
),
path("authors/", ListAuthorsAPIView.as_view(), name="list_authors"),
path(
"authors/<str:slug>",
RetrieveUpdateDestroyAuthorAPIView.as_view(),
name="retrieve_update_delete_author",
),
]

View File

@ -3,12 +3,19 @@
from akarpov.common.api.pagination import StandardResultsSetPagination
from akarpov.common.api.permissions import IsAdminOrReadOnly, IsCreatorOrReadOnly
from akarpov.music.api.serializers import (
AddSongToPlaylistSerializer,
AlbumSerializer,
AuthorSerializer,
FullAlbumSerializer,
FullAuthorSerializer,
FullPlaylistSerializer,
LikeDislikeSongSerializer,
ListPlaylistSerializer,
ListSongSerializer,
PlaylistSerializer,
SongSerializer,
)
from akarpov.music.models import Playlist, Song, SongUserRating
from akarpov.music.models import Album, Author, Playlist, Song, SongUserRating
class LikedSongsContextMixin(generics.GenericAPIView):
@ -26,11 +33,23 @@ def get_serializer_context(self):
class ListCreatePlaylistAPIView(generics.ListCreateAPIView):
permission_classes = [permissions.IsAuthenticated]
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
serializer_class = PlaylistSerializer
def get_queryset(self):
return Playlist.objects.filter(creator=self.request.user)
if self.request.user.is_authenticated:
return Playlist.objects.filter(creator=self.request.user).select_related(
"creator"
)
return Playlist.objects.filter(private=False).select_related("creator")
class ListPublicPlaylistAPIView(generics.ListAPIView):
permission_classes = [permissions.AllowAny]
serializer_class = PlaylistSerializer
def get_queryset(self):
return Playlist.objects.filter(private=False)
class RetrieveUpdateDestroyPlaylistAPIView(
@ -41,14 +60,11 @@ class RetrieveUpdateDestroyPlaylistAPIView(
permission_classes = [IsCreatorOrReadOnly]
serializer_class = FullPlaylistSerializer
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.object = None
def get_object(self):
if not self.object:
self.object = super().get_object()
return self.object
def get_queryset(self):
qs = Playlist.objects.filter(private=False)
if self.request.user.is_authenticated:
qs = Playlist.objects.filter(creator=self.request.user) | qs
return qs.select_related("creator")
class ListCreateSongAPIView(LikedSongsContextMixin, generics.ListCreateAPIView):
@ -59,9 +75,11 @@ class ListCreateSongAPIView(LikedSongsContextMixin, generics.ListCreateAPIView):
def get_queryset(self):
if self.request.user.is_authenticated:
return (
Song.objects.exclude(
Song.objects.all()
.exclude(
id__in=SongUserRating.objects.filter(
user=self.request.user
user=self.request.user,
like=False,
).values_list("song_id", flat=True)
)
.prefetch_related("authors")
@ -76,29 +94,149 @@ class RetrieveUpdateDestroySongAPIView(generics.RetrieveUpdateDestroyAPIView):
permission_classes = [IsCreatorOrReadOnly]
serializer_class = SongSerializer
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.object = None
def get_queryset(self):
return Song.objects.all()
def get_object(self):
if not self.object:
self.object = super().get_object()
return self.object
class ListSongPlaylistsAPIView(generics.ListAPIView):
serializer_class = ListPlaylistSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
return Playlist.objects.filter(
songs__song__slug=self.kwargs["slug"], creator=self.request.user
)
class ListLikedSongsAPIView(generics.ListAPIView):
serializer_class = ListSongSerializer
pagination_class = StandardResultsSetPagination
authentication_classes = [permissions.IsAuthenticated]
permission_classes = [permissions.IsAuthenticated]
def get_serializer_context(self):
context = super().get_serializer_context()
context["likes"] = True
return context
def get_queryset(self):
return (
Song.objects.cache()
.filter(
id__in=self.request.user.song_likes.objects.cache()
.all()
id__in=SongUserRating.objects.cache()
.filter(user=self.request.user, like=True)
.values_list("song_id", flat=True)
)
.prefetch_related("authors")
.select_related("album")
)
class ListDislikedSongsAPIView(generics.ListAPIView):
serializer_class = ListSongSerializer
pagination_class = StandardResultsSetPagination
permission_classes = [permissions.IsAuthenticated]
def get_serializer_context(self):
context = super().get_serializer_context()
context["likes"] = False
return context
def get_queryset(self):
return (
Song.objects.cache()
.filter(
id__in=SongUserRating.objects.cache()
.filter(user=self.request.user, like=True)
.values_list("song_id", flat=False)
)
.prefetch_related("authors")
.select_related("album")
)
class AddSongToPlaylistAPIView(generics.CreateAPIView):
serializer_class = AddSongToPlaylistSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
return Playlist.objects.filter(creator=self.request.user)
def get_serializer_context(self, **kwargs):
context = super().get_serializer_context()
context["create"] = True
return context
class RemoveSongFromPlaylistAPIView(generics.DestroyAPIView):
serializer_class = AddSongToPlaylistSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
return Playlist.objects.filter(creator=self.request.user)
def get_serializer_context(self, **kwargs):
context = super().get_serializer_context()
context["create"] = False
return context
class LikeSongAPIView(generics.CreateAPIView):
serializer_class = LikeDislikeSongSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
return SongUserRating.objects.all()
def get_serializer_context(self, **kwargs):
context = super().get_serializer_context()
context["like"] = True
return context
class DislikeSongAPIView(generics.CreateAPIView):
serializer_class = LikeDislikeSongSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
return SongUserRating.objects.all()
def get_serializer_context(self, **kwargs):
context = super().get_serializer_context()
context["like"] = False
return context
class ListAlbumsAPIView(generics.ListAPIView):
serializer_class = AlbumSerializer
pagination_class = StandardResultsSetPagination
permission_classes = [permissions.AllowAny]
def get_queryset(self):
return Album.objects.all()
class RetrieveUpdateDestroyAlbumAPIView(
LikedSongsContextMixin, generics.RetrieveUpdateDestroyAPIView
):
lookup_field = "slug"
lookup_url_kwarg = "slug"
permission_classes = [IsAdminOrReadOnly]
serializer_class = FullAlbumSerializer
class ListAuthorsAPIView(generics.ListAPIView):
serializer_class = AuthorSerializer
pagination_class = StandardResultsSetPagination
permission_classes = [permissions.AllowAny]
def get_queryset(self):
return Author.objects.all()
class RetrieveUpdateDestroyAuthorAPIView(
LikedSongsContextMixin, generics.RetrieveUpdateDestroyAPIView
):
lookup_field = "slug"
lookup_url_kwarg = "slug"
permission_classes = [IsAdminOrReadOnly]
serializer_class = FullAuthorSerializer

View File

@ -87,14 +87,14 @@ class SlugMeta:
class Playlist(ShortLinkModel, UserHistoryModel):
name = models.CharField(max_length=200)
private = models.BooleanField(default=False)
private = models.BooleanField(default=True)
creator = models.ForeignKey(
"users.User", related_name="playlists", on_delete=models.CASCADE
)
length = models.IntegerField(default=0)
def get_absolute_url(self):
return reverse("playlist:song", kwargs={"slug": self.slug})
return reverse("music:playlist", kwargs={"slug": self.slug})
def get_songs(self):
return self.songs.all().values("song")

View File

@ -3,7 +3,7 @@
from django.db.models.signals import post_delete, post_save, pre_save
from django.dispatch import receiver
from akarpov.music.models import Song, SongUserRating
from akarpov.music.models import PlaylistSong, Song, SongUserRating
@receiver(post_delete, sender=Song)
@ -34,3 +34,27 @@ def create_or_update_rating(sender, instance: SongUserRating, **kwargs):
else:
song.likes -= 1
song.save(update_fields=["likes"])
@receiver(post_delete, sender=SongUserRating)
def delete_rating(sender, instance: SongUserRating, **kwargs):
song = instance.song
if instance.like:
song.likes -= 1
else:
song.likes += 1
song.save(update_fields=["likes"])
@receiver(post_save, sender=PlaylistSong)
def update_playlist_length(sender, instance: PlaylistSong, **kwargs):
playlist = instance.playlist
playlist.length = playlist.songs.count()
playlist.save(update_fields=["length"])
@receiver(post_delete, sender=PlaylistSong)
def update_playlist_length_delete(sender, instance: PlaylistSong, **kwargs):
playlist = instance.playlist
playlist.length = playlist.songs.count()
playlist.save(update_fields=["length"])

View File

@ -26,6 +26,18 @@ def get_object_user(obj: Model) -> User | None:
return None
@lru_cache
def get_model_user_field(app_name: str, model: str) -> str:
model = apps.get_model(app_name, model)
if hasattr(model, "creator") and model.creator.field.related_model is User:
return "creator"
elif hasattr(model, "user") and model.user.field.related_model is User:
return "user"
elif hasattr(model, "owner") and model.owner.field.related_model is User:
return "owner"
return ""
@lru_cache
def get_app_verbose_name(app: str) -> str:
return apps.get_app_config(app).verbose_name