first backend frontend split

with docker compose file
nextjs something something
also includes migrations
This commit is contained in:
2025-06-20 14:06:00 +02:00
parent e7cc031588
commit dfc242084e
138 changed files with 1592 additions and 13 deletions
+8
View File
@@ -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 *
+24
View File
@@ -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)
+17
View File
@@ -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)
+32
View File
@@ -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)
+14
View File
@@ -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),
})
+34
View File
@@ -0,0 +1,34 @@
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)
+16
View File
@@ -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)
+48
View File
@@ -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)
+307
View File
@@ -0,0 +1,307 @@
from django.shortcuts import render
from django.views import View
from django.http import HttpResponse, HttpResponseRedirect, JsonResponse
from django.template.response import TemplateResponse
from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import Avg
from django.urls import reverse
import re
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
load_dotenv()
class AuthView(View):
def get(self, request):
if request.user.is_authenticated:
return HttpResponseRedirect('/vote/')
else:
return TemplateResponse(request, "Login.html", {})
class VoteView(LoginRequiredMixin, View):
def get(self, request, undo=None):
(track, skipped) = get_unvoted(request.user)
(vote_left, skip_left) = get_unvoted_count(request.user)
if track and not track.album.image:
if not track.album.image_url:
get_and_dither_image("https://picsum.photos/256", track.album, "image")
get_and_dither_image(track.album.image_url, track.album, "image")
return TemplateResponse(request, "Vote.html", {"track": track, "undo": undo, "skipped": skipped, "vote_left": vote_left, "skip_left": skip_left})
def post(self, request):
(profile,_) = Profile.objects.get_or_create(user=request.user)
try:
if not profile.can_vote:
raise Exception(f"{request.user} tried to vote after closing for {track}")
spotify_id = self.request.POST['spotify_id']
score = int(self.request.POST['vote'])
score = min(max(1, score), 5)
track = Track.objects.get(pk=spotify_id)
try:
prevote = Vote.objects.get(user=request.user, track=track)
except Vote.DoesNotExist:
pass
else:
if not prevote.skipped:
raise Exception(f"{request.user} already voted for {track}")
else:
prevote.delete()
vote = Vote.objects.create(track=track, user=request.user, points=score)
(profile,_) = Profile.objects.get_or_create(user=request.user)
profile.last_voted = track
profile.save()
return self.get(request, undo=vote)
except Exception as e:
print(e)
class NominateView(LoginRequiredMixin, View):
def get(self, request):
(profile,_) = Profile.objects.get_or_create(user=request.user)
profile.update_quota()
profile.save()
return TemplateResponse(request, "Nominate.html", {'end_time': os.getenv('DATE_NOM_END')})
def post(self, request):
(profile,_) = Profile.objects.get_or_create(user=request.user)
if not profile.can_nominate[0]:
return TemplateResponse(request, "Nominate.html", {"message_type": "error", "message": "Nee nee nee, dat gaan we dus even niet doen."})
try:
spotify_link = self.request.POST['spotify_link']
# Short url?
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)
spotify_id = m.group(1)
except:
return TemplateResponse(request, "Nominate.html", {"message_type": "error", "message": "Geef een geldige spotify link op!"})
try:
track = Track.all_tracks.get(pk=spotify_id)
if track.old:
track.old = False
track.nominated_by = request.user
track.save()
get_banter.delay(track.id)
return TemplateResponse(request, "Nominate.html", {
"message_type": "warning",
"message": f"Hey een klassieker, die stond al in {track.from_playlist}. Geen probleem, we proberen hem nog een keer dit jaar!",
"message_title": str(track)
})
return TemplateResponse(request, "Nominate.html", {
"message_type": "error",
"message_title": str(track),
"message": "Deze is al genomineerd dit jaar, probeer eens iets anders.",
})
except Track.DoesNotExist:
try:
json = spt.get_song_info(spotify_id)
except Exception as e:
print(e)
return TemplateResponse(request, "Nominate.html", {"message_type": "error", "message": "Fout bij het ophalen van het liedje."})
track = track_from_json(json)
track.nominated_by = request.user
track.save()
dup = track.is_duplicate_of()
if dup:
track.delete()
return TemplateResponse(request, "Nominate.html", {
"message_type": "error",
"message": "Die hadden we al knul.",
"message_title": str(dup)
})
get_banter.delay(track.id)
profile.quota -= 1
profile.save()
return TemplateResponse(request, "Nominate.html", {"message_type": "nominated", "message_title": str(track), "message": "Genomineerd!"})
class SettingsView(LoginRequiredMixin, View):
def get(self, request):
backgrounds = Background.objects.all()
(profile,created) = Profile.objects.get_or_create(user=request.user)
average_vote = Vote.objects.filter(user=request.user).aggregate(Avg("points", default=0))['points__avg']
context = {
"backgrounds": backgrounds,
"currentBackground": (profile.background.id if profile.background else 1),
"average": average_vote
}
spt_token = profile.get_or_update_spt()
if spt_token:
context["sptDisplayName"] = spt.get_profile(spt_token).get('display_name')
return TemplateResponse(request, "Settings.html", context)
def post(self, request):
try:
bg = request.POST.get('background', None)
unlink_spotify = request.POST.get('unlink-spotify', None)
(profile,_) = Profile.objects.get_or_create(user=request.user)
if bg:
bg = Background.objects.get(pk=request.POST['background'])
profile.background = bg
if unlink_spotify:
print("unlink spt")
profile.unlink_spt()
profile.save()
except:
pass
finally:
return self.get(request)
class UndoView(LoginRequiredMixin, View):
def get(self, request):
return HttpResponseRedirect('/vote/')
def post(self, request):
spotify_id = self.request.POST['spotify_id']
track = Track.objects.get(pk=spotify_id)
(profile,_) = Profile.objects.get_or_create(user=request.user)
if profile.last_voted != track:
raise Exception(f"{request.user} is not allowed to undo vote for {track}")
vote = None
try:
vote = Vote.objects.get(user=request.user, track=track)
except Vote.DoesNotExist:
raise Exception(f"{request.user} did not vote for {track}")
profile.last_voted = None
profile.save()
vote.delete()
return HttpResponseRedirect('/vote/')
class SkipView(LoginRequiredMixin, View):
def get(self, request):
return HttpResponseRedirect('/vote/')
def post(self, request):
spotify_id = self.request.POST['spotify_id']
track = Track.objects.get(pk=spotify_id)
(profile,_) = Profile.objects.get_or_create(user=request.user)
if not profile.can_vote:
raise Exception(f"{request.user} tried to vote after closing for {track}")
vote = None
try:
vote = Vote.objects.get(user=request.user, track=track)
except Vote.DoesNotExist:
pass
profile.last_voted = None
profile.save()
vote = Vote.objects.create(track=track, user=request.user, points=1, skipped=True)
profile.save()
return self.get(request)
class OverView(LoginRequiredMixin, View):
def get(self, request):
votes = Vote.objects.filter(user=request.user).select_related().distinct()
artists = {}
for vote in votes:
for artist in vote.track.artists.all():
if not artists.get(artist.name):
artists[artist.name] = {}
for artist in vote.track.album.artists.all():
if not artists.get(artist.name):
artists[artist.name] = {}
if not artists[artist.name].get(vote.track.album.name):
artists[artist.name][vote.track.album.name] = []
artists[artist.name][vote.track.album.name] += [vote.track]
artists_ordered = OrderedDict(sorted(artists.items()))
context = {
"artists": artists_ordered
}
return TemplateResponse(request, "Overview.html", context)
class PlaylistListView(LoginRequiredMixin, View):
def get(self, request, count=None):
context = {
"playlists": Playlist.objects.all()
}
return TemplateResponse(request, "PlaylistList.html", context)
def post(self, request):
name = self.request.POST.get('name', None)
if name:
new_playlist = Playlist(name=name)
return TemplateResponse(request, "PlaylistList.html")
return self.get(request, count=count)
class PlaylistDetailView(LoginRequiredMixin, View):
def get(self, request, count=None):
context = {}
if count:
#tracks = Track.objects.order_by('score')[count]
tracks = Track.objects.annotate(average_points=Avg('vote__points')).order_by('-average_points')[:count]
context["tracks"] = tracks
return TemplateResponse(request, "PlaylistDetail.html", context)
def post(self, request):
try:
count = int(self.request.POST['count'])
return self.get(request, count=count)
except Exception as e:
print(e)
return self.get(request)
class SpotifyView(LoginRequiredMixin, View):
def get(self, request):
url = spt.get_oauth_redirect()
return HttpResponseRedirect(url)
class SpotifyCallbackView(LoginRequiredMixin, View):
def get(self, request):
error = self.request.GET.get('error')
code = self.request.GET.get('code')
if error:
return TemplateResponse(request, "Settings.html", {"message": "Je moet wel accepteren"})
elif code:
(profile,_) = Profile.objects.get_or_create(user=request.user)
profile.init_spt(code)
return HttpResponseRedirect(reverse('settings'))
else:
url = spt.get_oauth_redirect()
return HttpResponseRedirect(url)
class PlaylistImportView(LoginRequiredMixin, View):
def get(self, request):
return HttpResponseRedirect('/settings/')
def post(self, request):
if not request.user.is_superuser:
return HttpResponseRedirect('/settings/')
playlist_link = self.request.POST['playlist_link']
m = re.match(r"^https:\/\/open.spotify.com\/playlist\/([a-zA-Z0-9]+)\??", playlist_link)
playlist_id = m.group(1)
print(playlist_id)
json = spt.get_playlist_info(playlist_id)
tracks = json["tracks"]["items"]
playlist_name = json["name"]
for track in tracks:
track_id = track["track"]["id"]
try:
json = spt.get_song_info(track_id)
track = track_from_json(json)
dup = track.is_duplicate_of()
if dup:
print(f"{track} hadden we al")
track.delete()
else:
track.old = True
track.from_playlist = playlist_name
track.save()
except Exception as e:
print(e)
return self.get(request)
class StroopwafelView(View):
def get(self, request):
nominated_count = Track.objects.all().count()
return JsonResponse(
{
"openDate": os.getenv('DATE_OPEN'),
"deadlineNominations": os.getenv('DATE_NOM_END'),
"deadlineVoting": os.getenv('DATE_VOTE_END'),
"stemCount": nominated_count
}
)