Use bootstrap-table for fancy ajax tables.

This commit is contained in:
Phillip Kühne 2019-08-18 23:55:59 +02:00
parent 41b2aa2ed1
commit 3ed8146b6f
11 changed files with 161 additions and 117 deletions

34
.vscode/launch.json vendored
View File

@ -6,6 +6,7 @@
"configurations": [ "configurations": [
{"name":"Python: Flask","type":"python","request":"launch","module":"flask","env":{"FLASK_APP":"app/main.py","FLASK_ENV":"development","FLASK_DEBUG":"1"},"args":["run","--no-debugger","--no-reload"],"jinja":true}, {"name":"Python: Flask","type":"python","request":"launch","module":"flask","env":{"FLASK_APP":"app/main.py","FLASK_ENV":"development","FLASK_DEBUG":"1"},"args":["run","--no-debugger","--no-reload"],"jinja":true},
{"name":"Python: Flask (with reload)","type":"python","request":"launch","module":"flask","env":{"FLASK_APP":"app/main.py","FLASK_ENV":"development","FLASK_DEBUG":"1"},"args":["run","--no-debugger"],"jinja":true}, {"name":"Python: Flask (with reload)","type":"python","request":"launch","module":"flask","env":{"FLASK_APP":"app/main.py","FLASK_ENV":"development","FLASK_DEBUG":"1"},"args":["run","--no-debugger"],"jinja":true},
{ {
@ -25,6 +26,24 @@
], ],
"jinja": true "jinja": true
}, },
{
"name": "Python: Flask (externally reachable)",
"type": "python",
"request": "launch",
"module": "flask",
"env": {
"FLASK_APP": "app/main.py",
"FLASK_ENV": "development",
"FLASK_DEBUG": "1"
},
"args": [
"run",
"--no-debugger",
"--no-reload",
"--host='0.0.0.0'"
],
"jinja": true
},
{ {
"name": "Python: Current File (Integrated Terminal)", "name": "Python: Current File (Integrated Terminal)",
"type": "python", "type": "python",
@ -65,21 +84,6 @@
], ],
"django": true "django": true
}, },
{
"name": "Python: Flask",
"type": "python",
"request": "launch",
"module": "flask",
"env": {
"FLASK_APP": "app.py"
},
"args": [
"run",
"--no-debugger",
"--no-reload"
],
"jinja": true
},
{ {
"name": "Python: Current File (External Terminal)", "name": "Python: Current File (External Terminal)",
"type": "python", "type": "python",

8
app/data_adapters.py Normal file
View File

@ -0,0 +1,8 @@
def dict_from_row(row):
return dict(zip(row.keys(), row))
def dict_from_rows(rows):
outlist=[]
for row in rows:
outlist.append(dict_from_row(row))
return outlist

View File

@ -74,6 +74,7 @@ def create_done_song_view():
def get_list(): def get_list():
conn = open_db() conn = open_db()
conn.row_factory = sqlite3.Row
cur = conn.cursor() cur = conn.cursor()
cur.execute("SELECT * FROM Liste") cur.execute("SELECT * FROM Liste")
return cur.fetchall() return cur.fetchall()
@ -141,6 +142,20 @@ def delete_entry(id):
return True return True
def delete_entries(ids):
idlist = []
for x in ids:
idlist.append( (x,) )
try:
conn = open_db()
cur = conn.cursor()
cur.executemany("DELETE FROM entries WHERE id=?", idlist)
conn.commit()
conn.close()
return cur.rowcount
except sqlite3.Error as error:
return -1
def delete_all_entries(): def delete_all_entries():
conn = open_db() conn = open_db()
cur = conn.cursor() cur = conn.cursor()

View File

@ -36,4 +36,4 @@ def setup_config(app):
json.dump(config, handle, indent=4, sort_keys=True) json.dump(config, handle, indent=4, sort_keys=True)
print("Wrote new config") print("Wrote new config")
app.config['BASIC_AUTH_USERNAME'] = config['username'] app.config['BASIC_AUTH_USERNAME'] = config['username']
app.config['BASIC_AUTH_PASSWORD'] = config['password'] app.config['BASIC_AUTH_PASSWORD'] = config['password']

View File

@ -1,6 +1,7 @@
from flask import Flask, render_template, Response, abort, request, redirect from flask import Flask, render_template, Response, abort, request, redirect
import helpers import app.helpers as helpers
import database import app.database as database
import app.data_adapters as data_adapters
import os, errno import os, errno
import json import json
from flask_basicauth import BasicAuth from flask_basicauth import BasicAuth
@ -29,6 +30,11 @@ def enqueue():
def songlist(): def songlist():
return render_template('songlist.html', list=database.get_song_list(), auth=basic_auth.authenticate()) return render_template('songlist.html', list=database.get_song_list(), auth=basic_auth.authenticate())
@app.route("/api/queue")
def queue_json():
list = data_adapters.dict_from_rows(database.get_list())
return Response(json.dumps(list, ensure_ascii=False).encode('utf-8'), mimetype='text/json')
@app.route("/plays") @app.route("/plays")
@basic_auth.required @basic_auth.required
def played_list(): def played_list():
@ -69,6 +75,20 @@ def delete_entry(entry_id):
return Response('{"status": "FAIL"}', mimetype='text/json') return Response('{"status": "FAIL"}', mimetype='text/json')
@app.route("/api/entries/delete", methods=['POST'])
@basic_auth.required
def delete_entries():
if not request.json:
print(request.data)
abort(400)
return
updates = database.delete_entries(request.json)
if updates >= 0:
return Response('{"status": "OK", "updates": '+str(updates)+'}', mimetype='text/json')
else:
return Response('{"status": "FAIL"}', mimetype='text/json', status=400)
@app.route("/api/entries/mark_sung/<entry_id>") @app.route("/api/entries/mark_sung/<entry_id>")
@basic_auth.required @basic_auth.required
def mark_sung(entry_id): def mark_sung(entry_id):

View File

@ -26,15 +26,11 @@ main {
.topbutton { .topbutton {
width: 100%; width: 100%;
margin-right: auto;
margin-bottom: 1rem;
} }
@media (min-width: 768px) { @media (min-width: 768px) {
.topbutton { .topbutton {
width: auto; width: auto;
margin-right: 1rem;
margin-bottom: auto;
} }
} }

View File

@ -11,6 +11,9 @@
<title>{% block title %}{% endblock %} - KaraoQueue</title> <title>{% block title %}{% endblock %} - KaraoQueue</title>
<!-- Bootstrap-Tables -->
<link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.15.3/dist/bootstrap-table.min.css">
<!-- Bootstrap core CSS --> <!-- Bootstrap core CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css"
integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous"> integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
@ -18,9 +21,10 @@
<!-- Custom styles for this template --> <!-- Custom styles for this template -->
<link href="static/css/style.css" rel="stylesheet"> <link href="static/css/style.css" rel="stylesheet">
<!-- Fontawesome Icons--> <!-- Fontawesome Icons -->
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.2/css/all.css" <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.2/css/all.css"
integrity="sha384-oS3vJWv+0UjzBfQzYUhtDYW+Pj2yciDJxpsK1OYPAYjqT085Qq/1cq5FLXAZQ7Ay" crossorigin="anonymous"> integrity="sha384-oS3vJWv+0UjzBfQzYUhtDYW+Pj2yciDJxpsK1OYPAYjqT085Qq/1cq5FLXAZQ7Ay" crossorigin="anonymous">
</head> </head>
<body> <body>
@ -88,6 +92,7 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootbox.js/4.4.0/bootbox.min.js" <script src="https://cdnjs.cloudflare.com/ajax/libs/bootbox.js/4.4.0/bootbox.min.js"
integrity="sha256-4F7e4JsAJyLUdpP7Q8Sah866jCOhv72zU5E8lIRER4w=" crossorigin="anonymous"> integrity="sha256-4F7e4JsAJyLUdpP7Q8Sah866jCOhv72zU5E8lIRER4w=" crossorigin="anonymous">
</script> </script>
<script src="https://unpkg.com/bootstrap-table@1.15.3/dist/bootstrap-table.min.js"></script>
{% block extrajs %}{% endblock %} {% block extrajs %}{% endblock %}
<script> <script>
$(document).ready(function () { $(document).ready(function () {

View File

@ -3,28 +3,19 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block title %}Warteliste{% endblock %} {% block title %}Warteliste{% endblock %}
{% block content %} {% block content %}
<a role="button" class="btn btn-primary btn-lg btn-block" href="/list">Eintragen</a> <a role="button" class="btn btn-primary btn-lg btn-block mb-2" href="/list">Eintragen</a>
<table class="table"> <table class="table"
data-toggle="table"
data-url="/api/queue"
data-pagination="true"
data-classes="table">
<thead> <thead>
<tr> <tr>
<th scope="col">Name</th> <th scope="col" data-field="Name">Name</th>
<th scope="col">Song</th> <th scope="col" data-field="Title">Song</th>
<th scope="col">Künstler</th> <th scope="col" data-field="Artist">Künstler</th>
</tr> </tr>
</thead> </thead>
{% for entry in list: %}
<tr>
<td>
{{ entry[0] }}
</td>
<td>
{{ entry[1] }}
</td>
<td>
{{ entry[2] }}
</td>
</tr>
{% endfor %}
</table> </table>
<a name="end"></a> <a name="end"></a>
{% endblock %} {% endblock %}

View File

@ -4,53 +4,37 @@
{% block title %}Warteliste-Admin{% endblock %} {% block title %}Warteliste-Admin{% endblock %}
{% block content %} {% block content %}
<div class="container"> <div class="container">
<div class="row"> <div id="toolbar">
<div class="card" style="width: 100%"> <button type="button" class="topbutton btn btn-danger" onclick="confirmDeleteSelectedEntries()"><i
<div class="card-body"> class="fas fa-trash mr-2"></i>Gewählte Einträge löschen</button>
<button type="button" class="topbutton btn btn-danger" <button type="button" class="topbutton btn btn-danger" onclick="confirmUpdateSongDatabase()"><i
onclick="confirmDeleteAllEntries()"><i class="fas fa-trash mr-2"></i>Alle Einträge löschen</button> class="fas fa-file-import mr-2"></i>Song-Datenbank
<button type="button" class="topbutton btn btn-danger" aktualisieren</button>
onclick="confirmUpdateSongDatabase()"><i class="fas fa-file-import mr-2"></i>Song-Datenbank
aktualisieren</button>
</div>
</div>
</div> </div>
<div class="row"> <table class="table"
<table class="table"> id="entrytable"
<thead> data-toggle="table"
<tr> data-search="true"
<th scope="col">Name</th> data-show-columns="true"
<th scope="col">Song</th> data-show-toggle="true"
<th scope="col">Künstler</th> data-multiple-select-row="true"
<th scope="col">Aktionen</th> data-click-to-select="true"
</tr> data-toolbar="#toolbar"
</thead> data-pagination="true"
{% for entry in list: %} data-show-extended-pagination="true"
data-classes="table table-hover"
data-url="/api/queue">
<thead>
<tr> <tr>
<td> <th data-field="state" data-checkbox="true"></th>
{{ entry[0] }} <th scope="col" data-field="Name">Name</th>
</td> <th scope="col" data-field="Title">Song</th>
<td> <th scope="col" data-field="Artist">Künstler</th>
{{ entry[1] }} <th scope="col" data-formatter="TableActions">Aktionen</th>
</td>
<td>
{{ entry[2] }}
</td>
<td>
<button type='button' class='btn btn-success'
data-toggle="tooltip" data-placement="top" title="Als gesungen markieren"
onclick='markEntryAsSung({{ entry[3] }})'><i
class="fas fa-check"></i></button>
<button type='button' class='btn btn-danger'
data-toggle="tooltip" data-placement="top" title="Eintrag löschen"
onclick='confirmDeleteEntry("{{ entry[0] }}",{{ entry[3] }})'><i
class="fas fa-trash"></i></button>
</td>
</tr> </tr>
{% endfor %} </thead>
</table> </table>
<a name="end"></a> <a name="end"></a>
</div>
</div> </div>
{% endblock %} {% endblock %}
{% block extrajs %} {% block extrajs %}
@ -65,9 +49,9 @@
} }
}) })
} }
function confirmDeleteAllEntries() { function confirmDeleteSelectedEntries() {
bootbox.confirm({ bootbox.confirm({
message: "Wirklich alle Eintragungen löschen?", message: "Wirklich gewählte Eintragungen löschen?",
buttons: { buttons: {
confirm: { confirm: {
label: 'Ja', label: 'Ja',
@ -80,7 +64,7 @@
}, },
callback: function(result){ callback: function(result){
if (result) { if (result) {
deleteAllEntries() DeleteSelectedEntries(getIdSelections())
} }
} }
}) })
@ -118,7 +102,7 @@
dataType: 'json', dataType: 'json',
async: false async: false
}); });
location.reload(); $("#entrytable").bootstrapTable('refresh')
} }
function markEntryAsSung(entry_id) { function markEntryAsSung(entry_id) {
$.ajax({ $.ajax({
@ -128,17 +112,24 @@
dataType: 'json', dataType: 'json',
async: false async: false
}); });
location.reload(); $("#entrytable").bootstrapTable('refresh')
} }
function deleteAllEntries() { function DeleteSelectedEntries(ids) {
$.ajax({ $.ajax({
type: 'GET', type: 'POST',
url: '/api/entries/delete_all', url: '/api/entries/delete',
data: JSON.stringify(ids), // or JSON.stringify ({name: 'jonas'}),
error: function() {
bootbox.alert({
message: "Fehler beim Löschen der Eintragungen.",
})
},
success: function() {
$("#entrytable").bootstrapTable('refresh')
},
contentType: "application/json", contentType: "application/json",
dataType: 'json', dataType: 'json'
async: false
}); });
location.reload();
} }
function updateSongDatabase(wait_dialog) { function updateSongDatabase(wait_dialog) {
$.ajax({ $.ajax({
@ -150,10 +141,18 @@
wait_dialog.modal('hide') wait_dialog.modal('hide')
bootbox.alert({ bootbox.alert({
message: data["status"], message: data["status"],
callback: function() {location.reload()} callback: function() {$("#entrytable").bootstrapTable('refresh')}
}) })
} }
}); });
} }
function TableActions (value, row, index) {
return "<button type='button' class='btn btn-success' data-toggle=\"tooltip\" data-placement=\"top\" title=\"Als gesungen markieren\" onclick='markEntryAsSung("+row.ID+")'><i class=\"fas fa-check\"></i></button>&nbsp;<button type='button' class='btn btn-danger' data-toggle=\"tooltip\" data-placement=\"top\" title=\"Eintrag löschen\" onclick='confirmDeleteEntry(\""+row.Name+"\","+row.ID+")'><i class=\"fas fa-trash\"></i></button>";
}
function getIdSelections() {
return $.map($("#entrytable").bootstrapTable('getSelections'), function (row) {
return row.ID
})
}
</script> </script>
{% endblock %} {% endblock %}

View File

@ -1,15 +1,23 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block title %}Abspielliste{% endblock %} {% block title %}Abspielliste{% endblock %}
{% block content %} {% block content %}
<div class="card admincontrols" style="width: 100%"> <div id="toolbar">
<div class="card-body"> <button type="button" class="topbutton btn btn-danger" onclick="confirmDeleteAllEntries()"><i
<button type="button" class="topbutton btn btn-danger" onclick="confirmDeleteAllEntries()"><i class="fas fa-trash mr-2"></i>Abspielliste löschen</button>
class="fas fa-trash mr-2"></i>Abspielliste löschen</button> <button type="button" class="topbutton btn btn-primary" onclick="exportPDF()"><i
<button type="button" class="topbutton btn btn-primary" onclick="exportPDF()"><i class="fas fa-file-pdf mr-2"></i>Als PDF herunterladen</button> class="fas fa-file-pdf mr-2"></i>Als PDF herunterladen</button>
<button type="button" class="topbutton btn btn-secondary" onclick="printPDF()"><i class="fas fa-print mr-2"></i>Drucken</button> <button type="button" class="topbutton btn btn-secondary" onclick="printPDF()"><i
</div> class="fas fa-print mr-2"></i>Drucken</button>
</div> </div>
<table class="table" id="table"> <table class="table"
id="table"
data-toggle="table"
data-search="true"
data-show-columns="true"
data-toolbar="#toolbar"
data-pagination="true"
data-classes="table table-bordered table-striped"
data-show-extended-pagination="true">
<thead> <thead>
<tr> <tr>
<th scope="col">Song</th> <th scope="col">Song</th>
@ -67,7 +75,8 @@
function exportPDF() { function exportPDF() {
var doc = new jsPDF(); var doc = new jsPDF();
doc.autoTable({ doc.autoTable({
html: '#table', head: [["Song","Wiedergaben"]],
body: createTableArray(),
theme: 'grid' theme: 'grid'
}); });
doc.save('Abspielliste.pdf'); doc.save('Abspielliste.pdf');
@ -76,11 +85,18 @@
function printPDF() { function printPDF() {
var doc = new jsPDF(); var doc = new jsPDF();
doc.autoTable({ doc.autoTable({
html: '#table', head: [["Song","Wiedergaben"]],
body: createTableArray(),
theme: 'grid' theme: 'grid'
}); });
doc.autoPrint(); doc.autoPrint();
doc.output('dataurlnewwindow'); doc.output('dataurlnewwindow');
} }
function createTableArray() {
var data = $("#table").bootstrapTable('getData')
out = data.map(x => [x["0"],x["1"]])
return out;
}
</script> </script>
{% endblock %} {% endblock %}

View File

@ -4,16 +4,6 @@
<input class="form-control" id="filter" type="text" placeholder="Suchen..."> <input class="form-control" id="filter" type="text" placeholder="Suchen...">
<table class="table"> <table class="table">
<tbody id="songtable"> <tbody id="songtable">
<!--{% for entry in (): %}
<tr>
<td>
{{ entry[0] }}
</td>
<td>
<button type="button" class="btn btn-primary"><i class="material-icons">queue_music</i></button>
</td>
</tr>
{% endfor %}-->
</tbody> </tbody>
</table> </table>
<div class="modal fade" id="enqueueModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" <div class="modal fade" id="enqueueModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel"