Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e7cc031588 | |||
| a54f8947bb | |||
| ce06b16881 | |||
| 18dd107cfd | |||
| 3de8234fb9 | |||
| 501044eef2 | |||
| ddb99f5056 | |||
| fed5612f46 | |||
| e41b9980a7 | |||
| 3223785a76 | |||
| 8b2bbfd4d3 | |||
| 5a432ae1ef |
@@ -26,6 +26,7 @@ INSTALLED_APPS = [
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'django_htmx',
|
||||
'rest_framework'
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
@@ -85,6 +86,16 @@ AUTHENTICATION_BACKENDS = (
|
||||
'django.contrib.auth.backends.ModelBackend',
|
||||
)
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': [
|
||||
'mozilla_django_oidc.contrib.drf.OIDCAuthentication',
|
||||
'rest_framework.authentication.SessionAuthentication',
|
||||
],
|
||||
'DEFAULT_PERMISSION_CLASSES': [
|
||||
'rest_framework.permissions.IsAuthenticated',
|
||||
]
|
||||
}
|
||||
|
||||
OIDC_RP_CLIENT_ID = os.getenv('OIDC_RP_CLIENT_ID')
|
||||
OIDC_RP_CLIENT_SECRET = os.getenv('OIDC_RP_CLIENT_SECRET')
|
||||
OIDC_OP_AUTHORIZATION_ENDPOINT = os.getenv('OIDC_OP_AUTHORIZATION_ENDPOINT')
|
||||
|
||||
@@ -55,14 +55,14 @@ class Profile(models.Model):
|
||||
"""Is this user allowed to nominate at this point in time?"""
|
||||
end_date = os.getenv('DATE_NOM_END')
|
||||
if self.user.is_superuser:
|
||||
return (True,)
|
||||
return (True, None)
|
||||
if end_date:
|
||||
end_date = datetime.fromisoformat(end_date)
|
||||
if end_date <= datetime.now(tz=UTC):
|
||||
return (False, "over")
|
||||
if self.quota < 1:
|
||||
return (False, "quota")
|
||||
return (True,)
|
||||
return (True, None)
|
||||
|
||||
@property
|
||||
def can_vote(self):
|
||||
|
||||
@@ -4,12 +4,92 @@ from random import choice
|
||||
from .album import Album
|
||||
from .artist import Artist
|
||||
from .vote import Vote
|
||||
from playlist.spotify import spt
|
||||
#from playlist.utils import track_from_json
|
||||
import re
|
||||
|
||||
class TrackManager(models.Manager):
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().filter(old=False)
|
||||
|
||||
def from_json(self, cls, json):
|
||||
try:
|
||||
return cls.objects.get(pk=json["id"])
|
||||
except cls.DoesNotExist:
|
||||
keys = [f.name for f in cls._meta.get_fields()]
|
||||
subset = {key:json[key] for key in set(keys) & set(json.keys())}
|
||||
return cls.objects.create(**subset)
|
||||
|
||||
def track_from_json(self, json):
|
||||
"""Creates the track, album and artist from spotify response."""
|
||||
# Album artists
|
||||
album_artists = []
|
||||
for artist in json["album"]["artists"]:
|
||||
album_artists.append(self.from_json(Artist, artist))
|
||||
del json["album"]["artists"]
|
||||
|
||||
# Album
|
||||
album = self.from_json(Album, json["album"])
|
||||
album.image_url = json["album"]["images"][0]["url"]
|
||||
album.save()
|
||||
del json["album"]
|
||||
for artist in album_artists:
|
||||
album.artists.add(artist)
|
||||
|
||||
# Track artists
|
||||
track_artists = []
|
||||
for artist in json["artists"]:
|
||||
track_artists.append(self.from_json(Artist, artist))
|
||||
del json["artists"]
|
||||
|
||||
# Track
|
||||
track = self.from_json(Track, json)
|
||||
for artist in track_artists:
|
||||
track.artists.add(artist)
|
||||
track.album = album
|
||||
track.save()
|
||||
return track
|
||||
|
||||
def create_from_spotify(self, spotify_link, profile):
|
||||
if re.match(r"^https:\/\/spotify.link.*", spotify_link):
|
||||
spotify_link = spt.unshort(spotify_link)
|
||||
|
||||
m = re.match(r"^https:\/\/open.spotify.com\/track\/([a-zA-Z0-9]+)\??", spotify_link)
|
||||
if m is None:
|
||||
return None, Exception("Invalid Spotify track URL")
|
||||
|
||||
spotify_id = m.group(1)
|
||||
|
||||
try:
|
||||
existing_track = self.model.all_tracks.get(pk=spotify_id)
|
||||
if existing_track.old:
|
||||
existing_track.old = False
|
||||
existing_track.user = profile.user
|
||||
existing_track.save()
|
||||
return (existing_track, "Updated old track")
|
||||
else:
|
||||
return (None, "Track already exists")
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
json = spt.get_song_info(spotify_id)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return (None, "Error fetching track")
|
||||
#return TemplateResponse(request, "Nominate.html", {"message_type": "error", "message": "Fout bij het ophalen van het liedje."})
|
||||
|
||||
track = self.track_from_json(json)
|
||||
track.nominated_by = profile.user
|
||||
track.save()
|
||||
dup = track.is_duplicate_of()
|
||||
if dup:
|
||||
track.delete()
|
||||
return (None, "Track already exists")
|
||||
#get_banter.delay(track.id)
|
||||
profile.quota -= 1
|
||||
profile.save()
|
||||
return (track, None)
|
||||
|
||||
class Track(models.Model):
|
||||
def __str__(self):
|
||||
s = f"'{self.name}'"
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||
|
||||
class Vote(models.Model):
|
||||
class Meta:
|
||||
unique_together = ["user", "track"]
|
||||
track = models.ForeignKey('Track', on_delete=models.CASCADE)
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
points = models.IntegerField()
|
||||
points = models.IntegerField(validators=[MinValueValidator(1), MaxValueValidator(5)])
|
||||
skipped = models.BooleanField(default=False)
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
from .track import *
|
||||
from .artist import *
|
||||
from .user import *
|
||||
from .album import *
|
||||
from .profile import *
|
||||
from .vote import *
|
||||
@@ -0,0 +1,12 @@
|
||||
from django.db.models.functions import Extract
|
||||
from playlist.models import Album
|
||||
from rest_framework import serializers
|
||||
|
||||
class AlbumSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = Album
|
||||
fields = '__all__'
|
||||
extra_kwargs = {
|
||||
'url': {'view_name': 'api-album', 'lookup_field': 'id'},
|
||||
'artists': {'view_name': 'api-artist', 'lookup_field': 'id'},
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
from playlist.models import Artist
|
||||
from rest_framework import serializers
|
||||
|
||||
class ArtistSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = Artist
|
||||
fields = ['id', 'name', 'url']
|
||||
extra_kwargs = {
|
||||
'url': {'view_name': 'api-artist', 'lookup_field': 'id'},
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
from playlist.models import Profile, Background
|
||||
from rest_framework import serializers
|
||||
|
||||
class BackgroundSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Background
|
||||
fields = '__all__'
|
||||
|
||||
class ProfileSerializer(serializers.ModelSerializer):
|
||||
background = BackgroundSerializer()
|
||||
|
||||
class Meta:
|
||||
model = Profile
|
||||
fields = '__all__'
|
||||
|
||||
class ProfileUpdateSerializer(serializers.ModelSerializer):
|
||||
background = serializers.PrimaryKeyRelatedField(many=False, queryset=Background.objects.all())
|
||||
class Meta:
|
||||
model = Profile
|
||||
fields = ['background']
|
||||
@@ -0,0 +1,13 @@
|
||||
from playlist.models import Track
|
||||
from rest_framework import serializers
|
||||
|
||||
class TrackSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = Track
|
||||
fields = '__all__'
|
||||
extra_kwargs = {
|
||||
'url': {'view_name': 'api-track', 'lookup_field': 'id'},
|
||||
'artists': {'view_name': 'api-artist', 'lookup_field': 'id'},
|
||||
'album': {'view_name': 'api-album', 'lookup_field': 'id'},
|
||||
'nominated_by': {'view_name': 'api-user', 'lookup_field': 'id'}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
from django.contrib.auth.models import User
|
||||
from rest_framework import serializers
|
||||
|
||||
class UserSerializer(serializers.HyperlinkedModelSerializer):
|
||||
# Add explicit reverse lookup to profile
|
||||
profile = serializers.HyperlinkedRelatedField(
|
||||
view_name='api-profile',
|
||||
lookup_field='id',
|
||||
read_only=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['url', 'id', 'username', 'email', 'profile']
|
||||
extra_kwargs = {
|
||||
'url': {'view_name': 'api-user', 'lookup_field': 'id'}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
from rest_framework import serializers
|
||||
from playlist.models import Vote, Track
|
||||
|
||||
class VoteSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = Vote
|
||||
fields = '__all__'
|
||||
extra_kwargs = {
|
||||
'url': {'view_name': 'api-vote', 'lookup_field': 'id'},
|
||||
'track': {'view_name': 'api-track', 'lookup_field': 'id'},
|
||||
'user': {'view_name': 'api-user', 'lookup_field': 'id'}
|
||||
}
|
||||
|
||||
class VoteSaveSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Vote
|
||||
fields = ['track', 'points', 'skipped']
|
||||
|
||||
def create(self, validated_data):
|
||||
"""
|
||||
Create a new vote. Deletes a previous skipped vote if it exists.
|
||||
"""
|
||||
user = self.context['request'].user
|
||||
track = validated_data['track']
|
||||
previous_vote = Vote.objects.filter(user=user, track=track).first()
|
||||
if previous_vote and previous_vote.skipped:
|
||||
previous_vote.delete()
|
||||
return Vote.objects.create(**validated_data)
|
||||
@@ -4,6 +4,24 @@ from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
|
||||
urlpatterns = [
|
||||
### API
|
||||
path('api/', views.api_root, name="api-root"),
|
||||
path('api/track/', views.TrackListView.as_view(), name="api-tracks"),
|
||||
path('api/track/<id>', views.TrackDetailView.as_view(), name="api-track"),
|
||||
path('api/artist/<id>', views.ArtistDetailView.as_view(), name="api-artist"),
|
||||
path('api/artist', views.ArtistListView.as_view(), name="api-artists"),
|
||||
path('api/album/<id>', views.AlbumDetailView.as_view(), name="api-album"),
|
||||
path('api/album/', views.AlbumListView.as_view(), name="api-albums"),
|
||||
path('api/vote/', views.VoteListView.as_view(), name="api-votes"),
|
||||
path('api/vote/<id>', views.VoteDetailView.as_view(), name="api-vote"),
|
||||
path('api/user/', views.UserListView.as_view(), name="api-users"),
|
||||
path('api/user/<int:id>', views.UserDetailView.as_view(), name="api-user"),
|
||||
path('api/user/<int:user_id>/votes', views.VoteListView.as_view(), name="api-user-votes"),
|
||||
path('api/profile/<int:id>', views.ProfileDetailView.as_view(), name="api-profile"),
|
||||
path('api/profile/background', views.BackgroundListView.as_view(), name="api-backgrounds"),
|
||||
path('api/profile/background/<int:id>', views.BackgroundDetailView.as_view(), name="api-background"),
|
||||
|
||||
### Classic
|
||||
path('track/', views.NominateView.as_view(), name="nominate"),
|
||||
path('vote/', views.VoteView.as_view(), name="vote"),
|
||||
path('vote/undo', views.UndoView.as_view(), name="undo"),
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
from .static import *
|
||||
from .api.root import *
|
||||
from .api.track import *
|
||||
from .api.artist import *
|
||||
from .api.user import *
|
||||
from .api.album import *
|
||||
from .api.profile import *
|
||||
from .api.vote import *
|
||||
@@ -0,0 +1,24 @@
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.authentication import SessionAuthentication
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from playlist.models import Album
|
||||
from playlist.serializers import AlbumSerializer
|
||||
|
||||
class AlbumListView(APIView):
|
||||
authentication_classes = [SessionAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request):
|
||||
albums = Album.objects.all()
|
||||
serializer = AlbumSerializer(albums, many=True, context={'request': request})
|
||||
return Response(serializer.data)
|
||||
|
||||
class AlbumDetailView(APIView):
|
||||
authentication_classes = [SessionAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request, id):
|
||||
album = Album.objects.get(pk=id)
|
||||
serializer = AlbumSerializer(album, context={'request': request})
|
||||
return Response(serializer.data)
|
||||
@@ -0,0 +1,17 @@
|
||||
from itertools import permutations
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from playlist.models import Artist
|
||||
from playlist.serializers import ArtistSerializer
|
||||
|
||||
class ArtistListView(APIView):
|
||||
def get(self, request):
|
||||
artists = Artist.objects.all()
|
||||
serializer = ArtistSerializer(artists, many=True, context={'request': request})
|
||||
return Response(serializer.data)
|
||||
|
||||
class ArtistDetailView(APIView):
|
||||
def get(self, request, id):
|
||||
artist = Artist.objects.get(pk=id)
|
||||
serializer = ArtistSerializer(artist, context={'request': request})
|
||||
return Response(serializer.data)
|
||||
@@ -0,0 +1,32 @@
|
||||
from pickle import BUILD
|
||||
from playlist import serializers
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from playlist.serializers import ProfileSerializer, ProfileUpdateSerializer, BackgroundSerializer
|
||||
from playlist.models import Profile, Background
|
||||
|
||||
class ProfileDetailView(APIView):
|
||||
def get(self, request, id):
|
||||
profile = Profile.objects.get(pk=id)
|
||||
serializer = ProfileSerializer(profile)
|
||||
return Response(serializer.data)
|
||||
|
||||
def put(self, request, id):
|
||||
profile = Profile.objects.get(pk=id)
|
||||
serializer = ProfileUpdateSerializer(profile, data=request.data)
|
||||
serializer.is_valid()
|
||||
serializer.save()
|
||||
return Response(serializer.data)
|
||||
#return Response()
|
||||
|
||||
class BackgroundListView(APIView):
|
||||
def get(self, request):
|
||||
backgrounds = Background.objects.all()
|
||||
serializer = BackgroundSerializer(backgrounds, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
class BackgroundDetailView(APIView):
|
||||
def get(self, request, id):
|
||||
background = Background.objects.get(pk=id)
|
||||
serializer = BackgroundSerializer(background)
|
||||
return Response(serializer.data)
|
||||
@@ -0,0 +1,14 @@
|
||||
from rest_framework.decorators import api_view, permission_classes
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.reverse import reverse
|
||||
|
||||
@api_view(['GET'])
|
||||
@permission_classes([])
|
||||
def api_root(request, format=None):
|
||||
return Response({
|
||||
'tracks': reverse('api-tracks', request=request, format=format),
|
||||
'artists': reverse('api-artists', request=request, format=format),
|
||||
'users': reverse('api-users', request=request, format=format),
|
||||
'albums': reverse('api-albums', request=request, format=format),
|
||||
'votes': reverse('api-votes', request=request, format=format),
|
||||
})
|
||||
@@ -0,0 +1,47 @@
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from playlist.models import Track, Profile
|
||||
from playlist.serializers import TrackSerializer
|
||||
from playlist.spotify import spt
|
||||
from playlist.tasks import get_banter, get_and_dither_image
|
||||
|
||||
class TrackListView(APIView):
|
||||
def get(self, request):
|
||||
tracks = Track.objects.all()
|
||||
serializer = TrackSerializer(tracks, many=True, context={'request': request})
|
||||
return Response(serializer.data)
|
||||
|
||||
def post(self, request):
|
||||
(profile, _) = Profile.objects.get_or_create(user=request.user)
|
||||
(can_nom, reason) = profile.can_nominate
|
||||
if not can_nom:
|
||||
return Response({'error': reason}, status=403)
|
||||
(track, message) = Track.objects.create_from_spotify(request.data['spotify_link'], profile)
|
||||
serializer = TrackSerializer(track, context={'request': request})
|
||||
return Response(serializer.data)
|
||||
|
||||
def delete(self, request):
|
||||
if request.user.is_superuser:
|
||||
Track.objects.all().delete()
|
||||
return Response(status=204)
|
||||
else:
|
||||
return Response({'error': 'Only superusers can delete all tracks'}, status=403)
|
||||
|
||||
class TrackDetailView(APIView):
|
||||
def get(self, request, id):
|
||||
track = Track.objects.get(pk=id)
|
||||
serializer = TrackSerializer(track, context={'request': request})
|
||||
return Response(serializer.data)
|
||||
def put(self, request, id):
|
||||
track = Track.objects.get(pk=id)
|
||||
serializer = TrackSerializer(track, data=request.data, context={'request': request})
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return Response(serializer.data)
|
||||
return Response(serializer.errors, status=400)
|
||||
def post(self, request):
|
||||
serializer = TrackSerializer(data=request.data, context={'request': request})
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return Response(serializer.data)
|
||||
return Response(serializer.errors, status=400)
|
||||
@@ -0,0 +1,16 @@
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from django.contrib.auth.models import User
|
||||
from playlist.serializers import UserSerializer
|
||||
|
||||
class UserListView(APIView):
|
||||
def get(self, request):
|
||||
users = User.objects.all()
|
||||
serializer = UserSerializer(users, many=True, context={'request': request})
|
||||
return Response(serializer.data)
|
||||
|
||||
class UserDetailView(APIView):
|
||||
def get(self, request, id):
|
||||
user = User.objects.get(pk=id)
|
||||
serializer = UserSerializer(user, context={'request': request})
|
||||
return Response(serializer.data)
|
||||
@@ -0,0 +1,48 @@
|
||||
from django.db import IntegrityError
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.authentication import SessionAuthentication
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from playlist.models import Vote, Profile
|
||||
from playlist.serializers import VoteSerializer, VoteSaveSerializer
|
||||
|
||||
class VoteListView(APIView):
|
||||
authentication_classes = [SessionAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request, user_id = None):
|
||||
if user_id:
|
||||
votes = Vote.objects.filter(user_id=user_id)
|
||||
else:
|
||||
votes = Vote.objects.all()
|
||||
serializer = VoteSerializer(votes, many=True, context={'request': request})
|
||||
return Response(serializer.data)
|
||||
|
||||
def post(self, request):
|
||||
(profile,_) = Profile.objects.get_or_create(user=request.user)
|
||||
if not profile.can_vote:
|
||||
return Response({'error': 'User cannot vote'}, status=403)
|
||||
serializer = VoteSaveSerializer(data=request.data, context={'request': request})
|
||||
if serializer.is_valid():
|
||||
try:
|
||||
serializer.save(user=request.user)
|
||||
return Response(serializer.data, status=201)
|
||||
except IntegrityError:
|
||||
return Response({'error': 'Duplicate vote'}, status=400)
|
||||
return Response(serializer.errors, status=400)
|
||||
|
||||
def delete(self, request):
|
||||
if request.user.is_superuser:
|
||||
Vote.objects.all().delete()
|
||||
return Response(status=204)
|
||||
else:
|
||||
return Response(status=403)
|
||||
|
||||
class VoteDetailView(APIView):
|
||||
authentication_classes = [SessionAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request, id):
|
||||
vote = Vote.objects.get(pk=id)
|
||||
serializer = VoteSerializer(vote, context={'request': request})
|
||||
return Response(serializer.data)
|
||||
@@ -10,10 +10,10 @@ import os
|
||||
from datetime import UTC, datetime
|
||||
from collections import OrderedDict
|
||||
from dotenv import load_dotenv
|
||||
from .models import Track, Artist, Album, Vote, Background, Profile, Playlist
|
||||
from .utils import track_from_json, get_unvoted, get_unvoted_count
|
||||
from .spotify import spt
|
||||
from .tasks import get_banter, get_and_dither_image
|
||||
from ..models import Track, Artist, Album, Vote, Background, Profile, Playlist
|
||||
from ..utils import track_from_json, get_unvoted, get_unvoted_count
|
||||
from ..spotify import spt
|
||||
from ..tasks import get_banter, get_and_dither_image
|
||||
|
||||
load_dotenv()
|
||||
|
||||
@@ -189,7 +189,7 @@ class SkipView(LoginRequiredMixin, View):
|
||||
pass
|
||||
profile.last_voted = None
|
||||
profile.save()
|
||||
vote = Vote.objects.create(track=track, user=request.user, points=0, skipped=True)
|
||||
vote = Vote.objects.create(track=track, user=request.user, points=1, skipped=True)
|
||||
profile.save()
|
||||
return self.get(request)
|
||||
|
||||
@@ -5,3 +5,7 @@ python-dotenv
|
||||
mozilla-django-oidc
|
||||
django-htmx
|
||||
celery
|
||||
djangorestframework
|
||||
markdown
|
||||
PyYAML
|
||||
Pygments
|
||||
|
||||
Reference in New Issue
Block a user