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 %}
|
||||
{% endblock %}
|
||||
|
Loading…
Reference in New Issue