Implement playlist import from Google spreadsheet
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)
|
Binary file not shown.
@ -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;
|
||||||
|
}
|
@ -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()})
|
@ -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…
Reference in New Issue