Implement playlist import from Google spreadsheet

main
Luca 2 years ago
parent 925f83d4bb
commit 1df7cdcf40

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

@ -0,0 +1,6 @@
from django.apps import AppConfig
class CoreConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "musicrate.core"

@ -0,0 +1,50 @@
# Generated by Django 4.1.4 on 2022-12-16 18:01
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name="Playlist",
fields=[
(
"spreadsheet_id",
models.CharField(max_length=44, primary_key=True, serialize=False),
),
("field_indices", models.JSONField()),
],
),
migrations.CreateModel(
name="Artist",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.TextField()),
("genre", models.TextField()),
("link_1", models.URLField(blank=True)),
("link_2", models.URLField(blank=True)),
("origin", models.TextField(blank=True)),
("comment", models.TextField(blank=True)),
(
"playlist",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="core.playlist"
),
),
],
),
]

@ -0,0 +1,16 @@
from django.db import models
# Create your models here.
class Playlist(models.Model):
spreadsheet_id = models.CharField(max_length=44, primary_key=True)
field_indices = models.JSONField()
class Artist(models.Model):
name = models.TextField()
genre = models.TextField()
link_1 = models.URLField(blank=True)
link_2 = models.URLField(blank=True)
origin = models.TextField(blank=True)
comment = models.TextField(blank=True)
playlist = models.ForeignKey(Playlist, on_delete=models.CASCADE)

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
viewBox="0 0 105.83592 105.83592"
height="28.00242mm"
width="28.00242mm"
xml:space="preserve"
version="1.1"
id="svg2"
sodipodi:docname="angel.svg"
inkscape:version="0.92.4 5da689c313, 2019-01-14"><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1362"
inkscape:window-height="721"
id="namedview869"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
units="mm"
inkscape:zoom="1.8311151"
inkscape:cx="25.694329"
inkscape:cy="4.5516744"
inkscape:window-x="0"
inkscape:window-y="22"
inkscape:window-maximized="0"
inkscape:current-layer="svg2" /><metadata
id="metadata8"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs6"><clipPath
id="clipPath18"
clipPathUnits="userSpaceOnUse"><path
id="path20"
d="M 0,1114.8 V 0 h 1741.01 v 1114.8 z"
inkscape:connector-curvature="0" /></clipPath></defs><path
inkscape:connector-curvature="0"
style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="M 25.83203,27.114244 C 11.6334,27.114244 0,38.747274 0,52.946274 c 0,14.19863 11.6334,25.775401 25.83203,25.775401 14.19875,0 25.77539,-11.576771 25.77539,-25.775401 0,-14.199 -11.57664,-25.83203 -25.77539,-25.83203 z m 54.22656,0 c -14.19863,0 -25.83008,11.63303 -25.83008,25.83203 0,14.19863 11.63145,25.775401 25.83008,25.775401 14.19875,0 25.77734,-11.576771 25.77734,-25.775401 0,-14.199 -11.57859,-25.83203 -25.77734,-25.83203 z m -62.14453,7.75391 h 6.16992 l 18.07617,18.07812 -18.02148,18.01953 h -6.28125 l 18.07617,-18.01953 z m 64.00195,0.0547 h 6.28125 l -18.07617,18.07618 18.07617,18.02148 H 81.9707 L 63.94921,52.999034 Z"
id="path22"
sodipodi:nodetypes="sssssssssscccccccccccccc" /></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

@ -0,0 +1,53 @@
@font-face {
font-family: "Maven Pro";
src: local("Maven Pro"), url(MavenPro-VariableFont:wght.ttf);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
align-items: center;
display: flex;
flex-direction: column;
font-family: "Maven Pro", sans-serif;
margin: 0 auto;
max-width: 800px;
min-height: 100vh;
padding: 1em;
}
body > * {
margin-bottom: 0.5em;
}
.error {
background: #c00;
border-radius: 0.5em;
color: #fff;
padding: 1em;
}
.form {
display: grid;
width: 100%;
}
.form * {
font-family: "Maven Pro", sans-serif;
grid-column: 2 / span 1;
margin: 0.125em 0.25em;
}
.form button {
padding: 0 0.5em;
width: max-content;
}
.form label {
grid-column: 1 / span 1;
text-align: right;
}

@ -5,6 +5,7 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0"> <meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>MusicRate</title> <title>MusicRate</title>
<link rel="icon" href="{% static 'kontakt.svg' %}" sizes="any" type="image/svg+xml">
<link rel="stylesheet" href="{% static 'style.css' %}"> <link rel="stylesheet" href="{% static 'style.css' %}">
</head> </head>
<body> <body>

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

@ -0,0 +1,6 @@
from django.apps import AppConfig
class HostConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "musicrate.host"

@ -0,0 +1,4 @@
from django import forms
class CreatePlaylistForm(forms.Form):
spreadsheet_url = forms.URLField(label='Link zur Tabelle')

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

@ -0,0 +1,14 @@
from django.conf import settings
from googleapiclient.discovery import build
service = build('sheets', 'v4', developerKey=settings.GOOGLE_API_KEY)
def get_sheet_data(spreadsheet_id, sheet=0):
spreadsheet = service.spreadsheets().get(spreadsheetId=spreadsheet_id).execute()
sheet = spreadsheet['sheets'][sheet]['properties']
sheet_name = sheet['title']
grid_properties = sheet['gridProperties']
req = service.spreadsheets().values().get(spreadsheetId=spreadsheet_id, range=f"{sheet_name}!R1C1:R{grid_properties['rowCount']}C{grid_properties['columnCount']}", majorDimension='ROWS')
return req.execute()['values']

@ -0,0 +1,21 @@
{% extends "core/base.html" %}
{% block body %}
<h1>Playlist erstellen</h1>
<form action="" method="post">
{% csrf_token %}
<div class="form">
{% for field in form %}
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
{{ field }}
{% endfor %}
<label for="submit_create_playlist"></label>
<button id="submit_create_playlist" type="submit">Erstellen</button>
</div>
</form>
{% if error %}
<div class="error">{{ error }}</div>
{% endif %}
{% endblock %}

@ -0,0 +1,5 @@
{% extends "core/base.html" %}
{% block body %}
<p>Diese Playlist enthält <b>{{ num_artists }} Künstler*innen</b>.</p>
{% endblock %}

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

@ -0,0 +1,8 @@
from django.urls import path
from . import views
urlpatterns = [
path('', views.create_playlist, name='create_playlist'),
path('<slug:playlist>', views.playlist, name='playlist'),
]

@ -0,0 +1,84 @@
from django.conf import settings
from django.shortcuts import get_object_or_404, redirect, render
from re import match
from .forms import CreatePlaylistForm
from .spreadsheet import get_sheet_data
from ..core.models import Artist, Playlist
# Create your views here.
def create_playlist(request):
form = CreatePlaylistForm()
if request.method == 'POST':
form = CreatePlaylistForm(request.POST)
if not form.is_valid():
return render(request, 'host/create_playlist.html', {'error': None, 'form': form})
m = match(r'https://docs.google.com/spreadsheets/d/([-0-9A-Z_a-z]{44})', form.cleaned_data['spreadsheet_url'])
if m == None:
return render(request, 'host/create_playlist.html', {'error': 'Der eingegebene Link führt nicht zu einer Google Tabelle.', 'form': CreatePlaylistForm()})
spreadsheet_id = m[1]
if Playlist.objects.filter(pk=spreadsheet_id).exists():
return redirect('playlist', spreadsheet_id)
playlist = Playlist(spreadsheet_id=spreadsheet_id)
data = get_sheet_data(spreadsheet_id)
mapping = {}
max_index = -1
artist_names = set()
artists = []
for row in data:
row = list(map(lambda s: s.strip(), row))
if not mapping:
if settings.REQUIRED_FIELDS[0] in row:
required_fields = set(settings.REQUIRED_FIELDS)
for index, heading in enumerate(row):
if heading in required_fields:
mapping[heading] = index
max_index = index
required_fields.remove(heading)
if len(required_fields) == 0:
break
if len(required_fields) != 0:
return render(request, 'host/create_playlist.html', {'error': 'Die Tabelle ist nicht im passenden Format.', 'form': CreatePlaylistForm()})
playlist.field_indices = mapping
playlist.save()
continue
values = {}
for field in settings.REQUIRED_FIELDS:
index = mapping[field]
model_field, blank = settings.SHEET_TO_MODEL[field]
if index >= len(row):
if not blank:
values = {}
break
continue
value = row[index]
if not (value or blank) or model_field == 'name' and value in artist_names:
values = {}
break
values[model_field] = value
if model_field == 'name':
artist_names.add(value)
if values:
artists.append(Artist(playlist=playlist, **values))
Artist.objects.bulk_create(artists)
return redirect('playlist', playlist=spreadsheet_id)
return render(request, 'host/create_playlist.html', {'error': None, 'form': form})
def playlist(request, playlist):
playlist = get_object_or_404(Playlist, pk=playlist)
return render(request, 'host/playlist.html', {'num_artists': playlist.artist_set.count()})

@ -41,6 +41,8 @@ INSTALLED_APPS = [
'django.contrib.sessions', 'django.contrib.sessions',
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'musicrate.core',
'musicrate.host',
'musicrate.vote', 'musicrate.vote',
] ]
@ -127,3 +129,15 @@ STATIC_URL = 'static/'
# https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field # https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
GOOGLE_API_KEY = getenv('GOOGLE_API_KEY')
REQUIRED_FIELDS = ['Band', 'Genre', 'Link 1', 'Link 2', 'Herkunft', 'Anmerkung']
SHEET_TO_MODEL = {
'Band': ('name', False),
'Genre': ('genre', False),
'Link 1': ('link_1', True),
'Link 2': ('link_2', True),
'Herkunft': ('origin', True),
'Anmerkung': ('comment', True),
}

@ -17,6 +17,6 @@ from django.contrib import admin
from django.urls import include, path from django.urls import include, path
urlpatterns = [ urlpatterns = [
path('', include('musicrate.vote.urls')), path('', include('musicrate.host.urls')),
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
] ]

@ -1,6 +0,0 @@
* {
box-sizing: border-box;
font-family: sans-serif;
margin: 0;
padding: 0;
}

@ -1,4 +1,4 @@
{% extends "base.html" %} {% extends "core/base.html" %}
{% block body %} {% block body %}
{% endblock %} {% endblock %}

Loading…
Cancel
Save