Compare commits

...

50 Commits

Author SHA1 Message Date
a54953ff0d fix copy functionality
Fixed copy functionality by no longer rendering the data into the
function call as string literal, but instead accessing it from the DOM
in the function call.
2023-06-30 15:28:46 +02:00
349eff9a09 strip whitespace from participant name in enqueue endpoint 2023-06-30 14:50:32 +02:00
b7a79462dc Add pipfile 2023-06-30 00:17:44 +02:00
0a038029f7 Merge pull request #68 from PhoenixTwoFive/61-properly-set-focus-on-opening-enqueue-modal
Set focus on singer name input when modal is shown
2023-06-30 00:16:09 +02:00
93e6606d6d Set focus on singer name input when modal is shown 2023-06-30 00:13:48 +02:00
1faa2614fa Merge branch 'legacy' of github.com:PhoenixTwoFive/karaoqueue into legacy 2023-04-28 13:41:51 +02:00
93c8a2cb7b Update Version 2023-04-28 13:41:39 +02:00
ddb1e0d2a0 Merge pull request #58 from PhoenixTwoFive:fix/legacy/fix-no-owned-entry
Fix error on no owned entries
2023-04-28 13:40:28 +02:00
e66e4a6c19 Fix error on no owned entries 2023-04-28 13:39:20 +02:00
08d0e5557b Update docker-compose to new version 2023-04-28 13:13:47 +02:00
a987dfc9d2 Merge branch 'legacy' of github.com:PhoenixTwoFive/karaoqueue into legacy 2023-04-28 13:03:57 +02:00
04511a91a1 Freeze versions to avoid Flask 2.3 deprecation problems 2023-04-28 13:03:53 +02:00
7da0dc19dc Merge pull request #57 from PhoenixTwoFive/feature/legacy/56-buttons-for-copying-to-clipboard-in-entry-list
Feature/legacy/56 buttons for copying to clipboard in entry list
2023-04-28 12:56:36 +02:00
d0d8e41b48 Indicate copyability of table text
Indicate copyability on click of table contents by highlighting
hovered element
2023-04-28 12:53:03 +02:00
f2b4611ea6 Remove problematic tooltip and debug log 2023-04-28 12:52:03 +02:00
b76fcfd8e4 Add Copy functionality and corresponding toasts 2023-04-28 12:40:28 +02:00
c9cbd24569 Merge pull request #55 from PhoenixTwoFive/feature/legacy/53-löschung-von-eigenen-einträgen-erlauben
Feature/legacy/53 löschung von eigenen einträgen erlauben
2023-04-27 00:18:37 +02:00
add528fb80 Löschung eigener Enträge implementiert. 2023-04-26 19:20:21 +02:00
865df5d588 Implement EventID to scope ClientIDs and Entry IDs
Implement an EventID saved in settings. Currently this is used to scope
clientIDs and entryIDs to an event. The client checks the event currently going on on
the server, and discards its localstorage (containing the clientID) if
it has changed
2023-04-26 18:08:03 +02:00
adebf35d08 Update lint.yaml
Remove action run on every push
2023-04-25 16:52:36 +02:00
d2caaac4bc Codecheck (#54)
* Add GitHub Action

* Add Linting

* Add .editorconfig
2023-04-25 16:46:43 +02:00
58dd0dd93b Add devcontainer.json setup 2023-04-02 17:21:56 +02:00
10717e753b Update Compose File 2023-03-31 20:58:47 +02:00
f32f02dc44 Fix Sorting in List view 2023-03-31 20:57:34 +02:00
3921a9ea76 Update docker-Compose 2023-03-30 22:56:09 +02:00
8c735866a3 Remove remnants of file based config 2023-03-30 22:45:08 +02:00
035394c36b Rephrase Information text 2023-03-30 22:37:39 +02:00
1d4f77ea19 Merge pull request #49 from PhoenixTwoFive/PhoenixTwoFive/issue45
Inform user that entries are closed when big button on main page is pressed and disabled
2023-03-30 22:34:52 +02:00
573f58d764 Inform user that entries are closed when big button on main page is pressed and disabled
Fixes #45
2023-03-30 22:33:50 +02:00
dff46404f6 Merge pull request #48 from PhoenixTwoFive:bugfix/47-misaligned-buttons
Fix button alignment
2023-03-30 22:28:57 +02:00
4fd58bc39f Fix button alignment 2023-03-30 22:28:12 +02:00
bf97e9e5e4 Fix container version 2023-03-30 22:21:42 +02:00
e08041338a Add persistent volume to mariadb container. 2023-03-30 21:18:40 +02:00
4c64144f3d Fix sticky Popups 2023-03-30 21:05:36 +02:00
fad21ba6c5 Fix DELETE statements 2023-03-30 19:30:27 +02:00
3f09b79844 Fix Dockerfile for "new" WSGI server 2023-03-30 19:30:13 +02:00
1bfe3b5d4b Update Dockerfile 2023-03-30 19:14:24 +02:00
f055a59a38 Fix Dockerfile with in-Container update 2023-03-30 18:14:40 +02:00
dc53d8a8b1 Fix Typos 2023-03-30 18:00:23 +02:00
fe71fa2d8c Fix Caching 2023-03-30 18:00:08 +02:00
7ef938a5ff Create new config with credentials from env vars 2023-03-30 17:51:20 +02:00
12207c1246 Admin credentials can be changed via settings 2023-03-30 17:39:44 +02:00
16cb9e7d5a Fix theme setting persistence 2023-03-30 17:39:13 +02:00
429ffddced Update data_adapters for sqlalchemy upgrade 2023-03-30 17:38:34 +02:00
ca73d57567 Update Requirements 2023-03-30 17:07:01 +02:00
cf6d586856 Overhaul database code 2023-03-30 17:06:52 +02:00
24458a78d0 Fix bugs 2023-03-30 17:06:36 +02:00
a2cf4fc47f Change .env.dev to reflect new settings storage 2023-03-30 17:05:46 +02:00
e84ff1a381 Change container registry to github 2023-03-30 17:05:17 +02:00
79156c539a Better Dark Mode 2023-03-30 04:43:53 +02:00
22 changed files with 861 additions and 321 deletions

View File

@ -0,0 +1,32 @@
{
"image": "mcr.microsoft.com/devcontainers/universal:2",
"hostRequirements": {
"cpus": 4
},
"waitFor": "onCreateCommand",
"updateContentCommand": "pip install -r requirements.txt",
"postCreateCommand": "",
"postAttachCommand": {
"server": "flask --debug run"
},
"portsAttributes": {
"5000": {
"label": "Application",
"onAutoForward": "openPreview"
}
},
"customizations": {
"codespaces": {
"openFiles": [
"templates/index.html"
]
},
"vscode": {
"extensions": [
"ms-python.python"
]
}
},
"forwardPorts": [5000]
}

36
.editorconfig Normal file
View File

@ -0,0 +1,36 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = false
insert_final_newline = false
[*.md]
trim_trailing_whitespace = true
[*.py]
indent_size = 4
[*.js]
indent_size = 2
[*.html]
indent_size = 2
[*.css]
indent_size = 2
[*.scss]
indent_size = 2
[*.yaml]
indent_size = 2
[*.yml]
indent_size = 2

View File

@ -7,8 +7,6 @@ MARIADB_PASSWORD=mariadb_karaoqueue_password
# Karaoqueue # Karaoqueue
DEPLOYMENT_PLATFORM=Docker DEPLOYMENT_PLATFORM=Docker
DBSTRING=mysql://karaoqueue:mariadb_karaoqueue_password@127.0.0.1:3306/karaoqueue DBSTRING="mysql+pymysql://karaoqueue:mariadb_karaoqueue_password@127.0.0.1:3306/karaoqueue?charset=utf8mb4"
BASIC_AUTH_USERNAME=admin INITIAL_USERNAME=admin
BASIC_AUTH_PASSWORD=change_me INITIAL_PASSWORD=changeme
ENTRY_QUOTA=3
MAX_QUEUE=20

3
.flake8 Normal file
View File

@ -0,0 +1,3 @@
[flake8]
ignore = E501
max-line-length = 120

23
.github/workflows/lint.yaml vendored Normal file
View File

@ -0,0 +1,23 @@
name: Lint
on: [pull_request]
jobs:
flake8_py3:
runs-on: ubuntu-latest
steps:
- name: Setup Python
uses: actions/setup-python@v4.6.0
with:
python-version: '3.10'
architecture: x64
- name: Checkout PyTorch
uses: actions/checkout@master
- name: Install flake8
run: pip install flake8
- name: Run flake8
uses: suo/flake8-github-action@releases/v1
with:
checkName: 'flake8_py3' # NOTE: this needs to be the same as the job name
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

2
.gitignore vendored
View File

@ -87,7 +87,7 @@ ipython_config.py
# However, in case of collaboration, if having platform-specific dependencies or dependencies # However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not # having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies. # install all needed dependencies.
#Pipfile.lock Pipfile.lock
# celery beat schedule file # celery beat schedule file
celerybeat-schedule celerybeat-schedule

16
.vscode/settings.json vendored
View File

@ -1,3 +1,17 @@
{ {
"python.pythonPath": "/usr/bin/python" "python.pythonPath": "/usr/bin/python",
"python.testing.unittestArgs": [
"-v",
"-s",
"./backend/tests",
"-p",
"*test.py"
],
"python.testing.pytestEnabled": false,
"python.testing.unittestEnabled": true,
"python.linting.pylintEnabled": false,
"python.linting.flake8Enabled": true,
"emmet.includeLanguages": {
"django-html": "html"
}
} }

View File

@ -1,5 +1,16 @@
FROM tiangolo/uwsgi-nginx-flask:python3.10 FROM tiangolo/meinheld-gunicorn-flask:python3.9
COPY ./backend /app RUN apt-key adv --recv-keys --keyserver keyserver.ubuntu.com 0xcbcb082a1bb943db
RUN curl -LsS https://downloads.mariadb.com/MariaDB/mariadb_repo_setup | bash
RUN pip install -r /app/requirements.txt RUN apt-get update
RUN apt-get upgrade -y
RUN apt-get dist-upgrade
COPY ./backend/requirements.txt /app/requirements.txt
RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt
RUN pip install --no-cache-dir -U meinheld
COPY ./backend /app

48
backend/Pipfile Normal file
View File

@ -0,0 +1,48 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
autopep8 = "==2.0.2"
beautifulsoup4 = "==4.12.0"
bs4 = "==0.0.1"
certifi = "==2022.12.7"
charset-normalizer = "==3.1.0"
click = "==8.1.3"
flake8 = "==6.0.0"
flask = "==2.2.3"
flask-basicauth = "==0.2.0"
greenlet = "==2.0.2"
gunicorn = "==20.1.0"
idna = "==3.4"
itsdangerous = "==2.1.2"
jinja2 = "==3.1.2"
mariadb = "==1.1.6"
markupsafe = "==2.1.2"
mccabe = "==0.7.0"
mysql = "==0.0.3"
mysqlclient = "==2.1.1"
numpy = "==1.24.2"
packaging = "==23.0"
pandas = "==1.5.3"
pycodestyle = "==2.10.0"
pyflakes = "==3.0.1"
pymysql = "==1.0.3"
python-dateutil = "==2.8.2"
pytz = "==2023.3"
requests = "==2.28.2"
six = "==1.16.0"
soupsieve = "==2.4"
sqlalchemy = "==2.0.7"
toml = "==0.10.2"
tomli = "==2.0.1"
typing-extensions = "==4.5.0"
urllib3 = "==1.26.15"
werkzeug = "==2.2.3"
[dev-packages]
[requires]
python_version = "3.9"
python_full_version = "3.9.17"

View File

@ -1,5 +1,5 @@
from flask import Flask, render_template, abort, request, redirect, send_from_directory, jsonify from flask import Flask, render_template, abort, request, redirect, send_from_directory, jsonify
from flask.wrappers import Request, Response from flask.wrappers import Response
import helpers import helpers
import database import database
import data_adapters import data_adapters
@ -12,6 +12,7 @@ app = Flask(__name__, static_url_path='/static')
basic_auth = BasicAuth(app) basic_auth = BasicAuth(app)
accept_entries = True accept_entries = True
@app.route("/") @app.route("/")
def home(): def home():
if basic_auth.authenticate(): if basic_auth.authenticate():
@ -36,11 +37,11 @@ def enqueue():
if not helpers.is_valid_uuid(client_id): if not helpers.is_valid_uuid(client_id):
print(request.data) print(request.data)
abort(400) abort(400)
name = request.json['name'] name = request.json['name'].strip()
song_id = request.json['id'] song_id = request.json['id']
if request.authorization: if request.authorization:
database.add_entry(name, song_id, client_id) entry_id = database.add_entry(name, song_id, client_id)
return Response('{"status":"OK"}', mimetype='text/json') return Response(f"""{{"status":"OK", "entry_id":{entry_id}}}""", mimetype='text/json')
else: else:
if helpers.get_accept_entries(app): if helpers.get_accept_entries(app):
if not request.json: if not request.json:
@ -54,8 +55,8 @@ def enqueue():
song_id = request.json['id'] song_id = request.json['id']
if database.check_queue_length() < int(app.config['MAX_QUEUE']): if database.check_queue_length() < int(app.config['MAX_QUEUE']):
if database.check_entry_quota(client_id) < int(app.config['ENTRY_QUOTA']): if database.check_entry_quota(client_id) < int(app.config['ENTRY_QUOTA']):
database.add_entry(name, song_id, client_id) entry_id = database.add_entry(name, song_id, client_id)
return Response('{"status":"OK"}', mimetype='text/json') return Response(f"""{{"status":"OK", "entry_id":{entry_id}}}""", mimetype='text/json')
else: else:
return Response('{"status":"Du hast bereits ' + str(database.check_entry_quota(client_id)) + ' Songs eingetragen, dies ist das Maximum an Einträgen die du in der Warteliste haben kannst."}', mimetype='text/json', status=423) return Response('{"status":"Du hast bereits ' + str(database.check_entry_quota(client_id)) + ' Songs eingetragen, dies ist das Maximum an Einträgen die du in der Warteliste haben kannst."}', mimetype='text/json', status=423)
else: else:
@ -83,6 +84,9 @@ def settings_post():
entryquota = request.form.get("entryquota") entryquota = request.form.get("entryquota")
maxqueue = request.form.get("maxqueue") maxqueue = request.form.get("maxqueue")
theme = request.form.get("theme") theme = request.form.get("theme")
username = request.form.get("username")
password = request.form.get("password")
changed_credentials = False
if entryquota.isnumeric() and int(entryquota) > 0: # type: ignore if entryquota.isnumeric() and int(entryquota) > 0: # type: ignore
app.config['ENTRY_QUOTA'] = int(entryquota) # type: ignore app.config['ENTRY_QUOTA'] = int(entryquota) # type: ignore
else: else:
@ -91,14 +95,21 @@ def settings_post():
app.config['MAX_QUEUE'] = int(maxqueue) # type: ignore app.config['MAX_QUEUE'] = int(maxqueue) # type: ignore
else: else:
abort(400) abort(400)
if theme in helpers.get_themes(): if theme is not None and theme in helpers.get_themes():
app.config['THEME'] = theme helpers.set_theme(app, theme)
else: else:
abort(400) abort(400)
if username != "" and username != app.config['BASIC_AUTH_USERNAME']:
app.config['BASIC_AUTH_USERNAME'] = username
changed_credentials = True
if password != "":
app.config['BASIC_AUTH_PASSWORD'] = password
changed_credentials = True
helpers.persist_config(app=app) helpers.persist_config(app=app)
if changed_credentials:
return render_template('settings.html', app=app, auth=basic_auth.authenticate()) return redirect("/")
else:
return render_template('settings.html', app=app, auth=basic_auth.authenticate(), themes=helpers.get_themes())
@app.route("/api/queue") @app.route("/api/queue")
@ -127,18 +138,18 @@ def songs():
@basic_auth.required @basic_auth.required
def update_songs(): def update_songs():
database.delete_all_entries() database.delete_all_entries()
helpers.reset_current_event_id(app)
status = database.import_songs( status = database.import_songs(
helpers.get_songs(helpers.get_catalog_url())) helpers.get_songs(helpers.get_catalog_url()))
print(status) print(status)
return Response('{"status": "%s" }' % status, mimetype='text/json') return Response('{"status": "%s" }' % status, mimetype='text/json')
@app.route("/api/songs/compl") @app.route("/api/songs/compl") # type: ignore
@nocache @nocache
def get_song_completions(input_string=""): def get_song_completions(input_string=""):
input_string = request.args.get('search', input_string) input_string = request.args.get('search', input_string)
if input_string != "": if input_string != "":
print(input_string)
result = [list(x) for x in database.get_song_completions(input_string=input_string)] result = [list(x) for x in database.get_song_completions(input_string=input_string)]
return jsonify(result) return jsonify(result)
@ -146,10 +157,29 @@ def get_song_completions(input_string=""):
return 400 return 400
@app.route("/api/entries/delete/<entry_id>") @app.route("/api/entries/delete/<entry_id>", methods=['GET'])
@nocache @nocache
@basic_auth.required @basic_auth.required
def delete_entry(entry_id): def delete_entry_admin(entry_id):
if database.delete_entry(entry_id):
return Response('{"status": "OK"}', mimetype='text/json')
else:
return Response('{"status": "FAIL"}', mimetype='text/json')
@app.route("/api/entries/delete/<entry_id>", methods=['POST'])
@nocache
def delete_entry_user(entry_id):
if not request.json:
print(request.data)
abort(400)
client_id = request.json['client_id']
if not helpers.is_valid_uuid(client_id):
print(request.data)
abort(400)
if database.get_raw_entry(entry_id)[3] != client_id: # type: ignore
print(request.data)
abort(403)
if database.delete_entry(entry_id): if database.delete_entry(entry_id):
return Response('{"status": "OK"}', mimetype='text/json') return Response('{"status": "OK"}', mimetype='text/json')
else: else:
@ -166,7 +196,7 @@ def delete_entries():
return return
updates = database.delete_entries(request.json) updates = database.delete_entries(request.json)
if updates >= 0: if updates >= 0:
return Response('{"status": "OK", "updates": '+str(updates)+'}', mimetype='text/json') return Response('{"status": "OK", "updates": ' + str(updates) + '}', mimetype='text/json')
else: else:
return Response('{"status": "FAIL"}', mimetype='text/json', status=400) return Response('{"status": "FAIL"}', mimetype='text/json', status=400)
@ -180,6 +210,7 @@ def mark_sung(entry_id):
else: else:
return Response('{"status": "FAIL"}', mimetype='text/json') return Response('{"status": "FAIL"}', mimetype='text/json')
@app.route("/api/entries/mark_transferred/<entry_id>") @app.route("/api/entries/mark_transferred/<entry_id>")
@nocache @nocache
@basic_auth.required @basic_auth.required
@ -195,7 +226,7 @@ def mark_transferred(entry_id):
@basic_auth.required @basic_auth.required
def set_accept_entries(value): def set_accept_entries(value):
if (value == '0' or value == '1'): if (value == '0' or value == '1'):
helpers.set_accept_entries(app,bool(int(value))) helpers.set_accept_entries(app, bool(int(value)))
return Response('{"status": "OK"}', mimetype='text/json') return Response('{"status": "OK"}', mimetype='text/json')
else: else:
return Response('{"status": "FAIL"}', mimetype='text/json', status=400) return Response('{"status": "FAIL"}', mimetype='text/json', status=400)
@ -205,7 +236,7 @@ def set_accept_entries(value):
@nocache @nocache
def get_accept_entries(): def get_accept_entries():
accept_entries = helpers.get_accept_entries(app) accept_entries = helpers.get_accept_entries(app)
return Response('{"status": "OK", "value": '+str(int(accept_entries))+'}', mimetype='text/json') return Response('{"status": "OK", "value": ' + str(int(accept_entries)) + '}', mimetype='text/json')
@app.route("/api/played/clear") @app.route("/api/played/clear")
@ -223,6 +254,7 @@ def clear_played_songs():
@basic_auth.required @basic_auth.required
def delete_all_entries(): def delete_all_entries():
if database.delete_all_entries(): if database.delete_all_entries():
helpers.reset_current_event_id(app)
return Response('{"status": "OK"}', mimetype='text/json') return Response('{"status": "OK"}', mimetype='text/json')
else: else:
return Response('{"status": "FAIL"}', mimetype='text/json') return Response('{"status": "FAIL"}', mimetype='text/json')
@ -234,11 +266,16 @@ def admin():
return redirect("/", code=303) return redirect("/", code=303)
@app.route("/api/events/current")
@nocache
def get_current_event():
return Response('{"status": "OK", "event": "' + helpers.get_current_event_id(app) + '"}', mimetype='text/json')
@app.before_first_request @app.before_first_request
def activate_job(): def activate_job():
helpers.load_dbconfig(app) helpers.load_dbconfig(app)
helpers.load_version(app) helpers.load_version(app)
helpers.create_data_directory()
database.create_entry_table() database.create_entry_table()
database.create_song_table() database.create_song_table()
database.create_done_song_table() database.create_done_song_table()
@ -248,17 +285,17 @@ def activate_job():
helpers.setup_config(app) helpers.setup_config(app)
@app.after_request @app.after_request
def add_header(response): def add_header(response):
""" """
Add headers to both force latest IE rendering engine or Chrome Frame, Add headers to both force latest IE rendering engine or Chrome Frame,
and also to cache the rendered page for 10 minutes. and also to cache the rendered page for 10 minutes.
""" """
if not 'Cache-Control' in response.headers: if 'Cache-Control' not in response.headers:
response.headers['Cache-Control'] = 'private, max-age=600' response.headers['Cache-Control'] = 'private, max-age=600, no-cache, must-revalidate'
return response return response
@app.context_processor @app.context_processor
def inject_version(): def inject_version():
return dict(karaoqueue_version=app.config['VERSION']) return dict(karaoqueue_version=app.config['VERSION'])

View File

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

View File

@ -1,11 +1,10 @@
# -*- coding: utf_8 -*- # -*- coding: utf_8 -*-
from email.mime import base from sqlalchemy import create_engine, engine, text
from MySQLdb import Connection
from sqlalchemy import create_engine, engine
import pandas import pandas
from io import StringIO from io import StringIO
from flask import current_app from flask import current_app
import uuid
song_table = "songs" song_table = "songs"
entry_table = "entries" entry_table = "entries"
@ -18,8 +17,8 @@ sql_engine = None
def get_db_engine() -> engine.base.Engine: def get_db_engine() -> engine.base.Engine:
global sql_engine global sql_engine
if (not sql_engine): if (not sql_engine):
print(current_app.config.get("DBCONNSTRING")) sql_engine = create_engine(
sql_engine = create_engine(current_app.config.get("DBCONNSTRING")) # type: ignore current_app.config.get("DBCONNSTRING")) # type: ignore
return sql_engine return sql_engine
@ -28,147 +27,193 @@ def import_songs(song_csv):
df = pandas.read_csv(StringIO(song_csv), sep=';') df = pandas.read_csv(StringIO(song_csv), sep=';')
with get_db_engine().connect() as conn: with get_db_engine().connect() as conn:
df.to_sql(song_table, conn, if_exists='replace', df.to_sql(song_table, conn, if_exists='replace',
index=False) index=False)
cur = conn.execute("SELECT Count(Id) FROM songs") cur = conn.execute(text("SELECT Count(Id) FROM songs"))
num_songs = cur.fetchone()[0] # type: ignore num_songs = cur.fetchone()[0] # type: ignore
conn.commit()
print("Imported songs ({} in Database)".format(num_songs)) print("Imported songs ({} in Database)".format(num_songs))
return("Imported songs ({} in Database)".format(num_songs)) return ("Imported songs ({} in Database)".format(num_songs))
def create_entry_table(): def create_entry_table():
with get_db_engine().connect() as conn: with get_db_engine().connect() as conn:
conn.execute('CREATE TABLE IF NOT EXISTS '+entry_table + stmt = text(
' (ID INTEGER PRIMARY KEY NOT NULL AUTO_INCREMENT, Song_Id INTEGER NOT NULL, Name VARCHAR(255), Client_Id VARCHAR(36), Transferred INTEGER DEFAULT 0)') f'CREATE TABLE IF NOT EXISTS `{entry_table}` (ID INTEGER PRIMARY KEY NOT NULL AUTO_INCREMENT, Song_Id INTEGER NOT NULL, Name VARCHAR(255), Client_Id VARCHAR(36), Transferred INTEGER DEFAULT 0)')
conn.execute(stmt)
conn.commit()
def create_done_song_table(): def create_done_song_table():
with get_db_engine().connect() as conn: with get_db_engine().connect() as conn:
conn.execute('CREATE TABLE IF NOT EXISTS '+done_table + stmt = text(
' (Song_Id INTEGER PRIMARY KEY NOT NULL, Plays INTEGER)') f'CREATE TABLE IF NOT EXISTS `{done_table}` (Song_Id INTEGER PRIMARY KEY NOT NULL, Plays INTEGER)')
conn.execute(stmt)
conn.commit()
def create_song_table(): def create_song_table():
with get_db_engine().connect() as conn: with get_db_engine().connect() as conn:
conn.execute("CREATE TABLE IF NOT EXISTS `"+song_table+"""` ( stmt = text(f"""CREATE TABLE IF NOT EXISTS `{song_table}` (
`Id` INTEGER, `Id` INTEGER,
`Title` TEXT, `Title` TEXT,
`Artist` TEXT, `Artist` TEXT,
`Year` INTEGER, `Year` VARCHAR(4),
`Duo` INTEGER, `Duo` BOOLEAN,
`Explicit` INTEGER, `Explicit` INTEGER,
`Date Added` TEXT, `Date Added` TIMESTAMP,
`Styles` TEXT, `Styles` TEXT,
`Languages` TEXT `Languages` TEXT
)""") )""")
conn.execute(stmt)
conn.commit()
def create_list_view(): def create_list_view():
with get_db_engine().connect() as conn: with get_db_engine().connect() as conn:
conn.execute("""CREATE OR REPLACE VIEW `Liste` AS stmt = text("""CREATE OR REPLACE VIEW `Liste` AS
SELECT Name, Title, Artist, entries.Id AS entry_ID, songs.Id AS song_ID, entries.Transferred SELECT Name, Title, Artist, entries.Id AS entry_ID, songs.Id AS song_ID, entries.Transferred
FROM entries, songs FROM entries, songs
WHERE entries.Song_Id=songs.Id""") WHERE entries.Song_Id=songs.Id
ORDER BY entries.Id ASC
""")
conn.execute(stmt)
conn.commit()
def create_done_song_view(): def create_done_song_view():
with get_db_engine().connect() as conn: with get_db_engine().connect() as conn:
conn.execute("""CREATE OR REPLACE VIEW `Abspielliste` AS stmt = text("""CREATE OR REPLACE VIEW `Abspielliste` AS
SELECT CONCAT(Artist," - ", Title) AS Song, Plays AS Wiedergaben SELECT CONCAT(Artist," - ", Title) AS Song, Plays AS Wiedergaben
FROM songs, done_songs FROM songs, done_songs
WHERE done_songs.Song_Id=songs.Id""") WHERE done_songs.Song_Id=songs.Id""")
conn.execute(stmt)
conn.commit()
def create_config_table(): def create_config_table():
with get_db_engine().connect() as conn: with get_db_engine().connect() as conn:
conn.execute("""CREATE TABLE IF NOT EXISTS `config` ( stmt = text("""CREATE TABLE IF NOT EXISTS `config` (
`Key` VARCHAR(50) NOT NULL PRIMARY KEY, `Key` VARCHAR(50) NOT NULL PRIMARY KEY,
`Value` TEXT `Value` TEXT
)""") )""")
conn.execute(stmt)
conn.commit()
def get_list(): def get_list():
with get_db_engine().connect() as conn: with get_db_engine().connect() as conn:
cur = conn.execute("SELECT * FROM Liste") stmt = text("SELECT * FROM Liste")
cur = conn.execute(stmt)
return cur.fetchall() return cur.fetchall()
def get_played_list(): def get_played_list():
with get_db_engine().connect() as conn: with get_db_engine().connect() as conn:
cur = conn.execute("SELECT * FROM Abspielliste") stmt = text("SELECT * FROM Abspielliste")
cur = conn.execute(stmt)
return cur.fetchall() return cur.fetchall()
def get_song_list(): def get_song_list():
with get_db_engine().connect() as conn: with get_db_engine().connect() as conn:
cur = conn.execute( stmt = text("SELECT Artist || \" - \" || Title AS Song, Id FROM songs;")
"SELECT Artist || \" - \" || Title AS Song, Id FROM songs;") cur = conn.execute(stmt)
return cur.fetchall() return cur.fetchall()
def get_song_completions(input_string): def get_song_completions(input_string):
with get_db_engine().connect() as conn: with get_db_engine().connect() as conn:
# Don't look, it burns... prepared_string = f"%{input_string.upper()}%"
prepared_string = "%{0}%".format( stmt = text(
input_string).upper() # "Test" -> "%TEST%" "SELECT CONCAT(Artist, ' - ', Title) AS Song, Id FROM songs WHERE CONCAT(Artist, ' - ', Title) LIKE :prepared_string LIMIT 20;")
print(prepared_string)
cur = conn.execute( cur = conn.execute(
"SELECT CONCAT(Artist,\" - \",Title) AS Song, Id FROM songs WHERE CONCAT(Artist,\" - \",Title) LIKE (%s) LIMIT 20;", [prepared_string]) stmt, {"prepared_string": prepared_string}) # type: ignore
return cur.fetchall() return cur.fetchall()
def add_entry(name, song_id, client_id): def add_entry(name, song_id, client_id):
with get_db_engine().connect() as conn: with get_db_engine().connect() as conn:
conn.execute( stmt = text(
"INSERT INTO entries (Song_Id,Name,Client_Id) VALUES(%s,%s,%s);", (song_id, name, client_id)) "INSERT INTO entries (Song_Id,Name,Client_Id) VALUES (:par_song_id,:par_name,:par_client_id) RETURNING entries.ID;")
return cur = conn.execute(stmt, {"par_song_id": song_id, "par_name": name,
"par_client_id": client_id}) # type: ignore
conn.commit()
return cur.fetchone()[0] # type: ignore
def add_sung_song(entry_id): def add_sung_song(entry_id):
with get_db_engine().connect() as conn: with get_db_engine().connect() as conn:
cur = conn.execute( stmt = text("SELECT Song_Id FROM entries WHERE Id=:par_entry_id")
"""SELECT Song_Id FROM entries WHERE Id=%s""", (entry_id,)) cur = conn.execute(stmt, {"par_entry_id": entry_id}) # type: ignore
song_id = cur.fetchone()[0] # type: ignore song_id = cur.fetchone()[0] # type: ignore
conn.execute("""INSERT INTO done_songs (Song_Id, Plays) VALUES("""+str(song_id)+""",1) ON DUPLICATE KEY UPDATE Plays=Plays + 1;""") stmt = text(
"INSERT INTO done_songs (Song_Id,Plays) VALUES (:par_song_id,1) ON DUPLICATE KEY UPDATE Plays=Plays + 1;")
conn.execute(stmt, {"par_song_id": song_id}) # type: ignore
conn.commit()
delete_entry(entry_id) delete_entry(entry_id)
return True return True
def toggle_transferred(entry_id): def toggle_transferred(entry_id):
with get_db_engine().connect() as conn: with get_db_engine().connect() as conn:
cur = conn.execute( cur = conn.execute(text("SELECT Transferred FROM entries WHERE ID = :par_entry_id"),
"SELECT Transferred FROM entries WHERE ID =%s", (entry_id,)) {"par_entry_id": entry_id}) # type: ignore
marked = cur.fetchall()[0][0] marked = cur.fetchall()[0][0]
if(marked == 0): if (marked == 0):
conn.execute( conn.execute(text("UPDATE entries SET Transferred = 1 WHERE ID = :par_entry_id"),
"UPDATE entries SET Transferred = 1 WHERE ID =%s", (entry_id,)) {"par_entry_id": entry_id}) # type: ignore
else: else:
conn.execute( conn.execute(text("UPDATE entries SET Transferred = 0 WHERE ID = :par_entry_id"),
"UPDATE entries SET Transferred = 0 WHERE ID =%s", (entry_id,)) {"par_entry_id": entry_id}) # type: ignore
conn.commit()
return True return True
def check_entry_quota(client_id): def check_entry_quota(client_id):
with get_db_engine().connect() as conn: with get_db_engine().connect() as conn:
cur = conn.execute( cur = conn.execute(text("SELECT Count(*) FROM entries WHERE entries.Client_Id = :par_client_id"),
"SELECT Count(*) FROM entries WHERE entries.Client_Id = %s", (client_id,)) {"par_client_id": client_id}) # type: ignore
return cur.fetchall()[0][0] return cur.fetchall()[0][0]
def check_queue_length(): def check_queue_length():
with get_db_engine().connect() as conn: with get_db_engine().connect() as conn:
cur = conn.execute("SELECT Count(*) FROM entries") cur = conn.execute(text("SELECT Count(*) FROM entries"))
return cur.fetchall()[0][0] return cur.fetchall()[0][0]
def clear_played_songs(): def clear_played_songs():
with get_db_engine().connect() as conn: with get_db_engine().connect() as conn:
conn.execute("DELETE FROM done_songs") conn.execute(text("DELETE FROM done_songs"))
conn.commit()
return True return True
def get_entry(id):
try:
with get_db_engine().connect() as conn:
cur = conn.execute(text("SELECT * FROM Liste WHERE entry_ID = :par_id"),
{"par_id": id}) # type: ignore
return cur.fetchall()[0]
except Exception:
return None
def get_raw_entry(id):
try:
with get_db_engine().connect() as conn:
cur = conn.execute(text("SELECT * FROM entries WHERE ID = :par_id"),
{"par_id": id}) # type: ignore
return cur.fetchall()[0]
except Exception:
return None
def delete_entry(id): def delete_entry(id):
with get_db_engine().connect() as conn: with get_db_engine().connect() as conn:
conn.execute("DELETE FROM entries WHERE id=%s", (id,)) conn.execute(text("DELETE FROM entries WHERE id= :par_id"), {
"par_id": id}) # type: ignore
conn.commit()
return True return True
@ -178,32 +223,75 @@ def delete_entries(ids):
idlist.append((x,)) idlist.append((x,))
try: try:
with get_db_engine().connect() as conn: with get_db_engine().connect() as conn:
cur = conn.execute("DELETE FROM entries WHERE id=%s", idlist) cur = conn.execute(text("DELETE FROM entries WHERE id= :par_id"), {
"par_id": idlist})
conn.commit()
return cur.rowcount return cur.rowcount
except Exception as error: except Exception:
return -1 return -1
def delete_all_entries() -> bool: def delete_all_entries() -> bool:
with get_db_engine().connect() as conn: with get_db_engine().connect() as conn:
conn.execute("DELETE FROM entries") conn.execute(text("DELETE FROM entries"))
conn.commit()
return True return True
def get_config(key: str) -> str: def get_config(key: str) -> str:
with get_db_engine().connect() as conn: try:
cur = conn.execute("SELECT `Value` FROM config WHERE `Key`=%s", (key,)) with get_db_engine().connect() as conn:
return cur.fetchall()[0][0] cur = conn.execute(
text("SELECT `Value` FROM config WHERE `Key`= :par_key"), {"par_key": key}) # type: ignore
conn.commit()
return cur.fetchall()[0][0]
except IndexError:
return ""
def set_config(key: str, value: str) -> bool: def set_config(key: str, value: str) -> bool:
print(f"Setting config {key} to {value}")
with get_db_engine().connect() as conn: with get_db_engine().connect() as conn:
conn.execute("INSERT INTO config (`Key`, `Value`) VALUES (%s,%s) ON DUPLICATE KEY UPDATE `Value`=%s", (key, value, value)) conn.execute(text(
"INSERT INTO config (`Key`, `Value`) VALUES ( :par_key , :par_value) ON DUPLICATE KEY UPDATE `Value`= :par_value"),
{"par_key": key, "par_value": value}
) # type: ignore
conn.commit()
return True return True
def get_config_list() -> dict: def get_config_list() -> dict:
with get_db_engine().connect() as conn: with get_db_engine().connect() as conn:
cur = conn.execute("SELECT * FROM config") cur = conn.execute(text("SELECT * FROM config"))
result_dict = {} result_dict = {}
for row in cur.fetchall(): for row in cur.fetchall():
result_dict[row[0]] = row[1] result_dict[row[0]] = row[1]
return result_dict return result_dict
def check_config_table() -> bool:
with get_db_engine().connect() as conn:
if conn.dialect.has_table(conn, 'config'):
# type: ignore
# type: ignore
if (conn.execute(text("SELECT COUNT(*) FROM config")).fetchone()[0] > 0): # type: ignore
return True
else:
return False
else:
return False
def init_event_id() -> bool:
if not get_config("EventID"):
set_config("EventID", str(uuid.uuid4()))
return True
def reset_event_id() -> bool:
set_config("EventID", str(uuid.uuid4()))
return True
def get_event_id() -> str:
return get_config("EventID")

View File

@ -1,6 +1,5 @@
import requests import requests
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
import json
import os import os
import uuid import uuid
from flask import make_response, Flask from flask import make_response, Flask
@ -8,24 +7,20 @@ from functools import wraps, update_wrapper
from datetime import datetime from datetime import datetime
import database import database
data_directory = "data"
config_file = data_directory+"/config.json"
def create_data_directory():
if not os.path.exists(data_directory):
os.makedirs(data_directory)
def get_catalog_url(): def get_catalog_url():
r = requests.get('https://www.karafun.de/karaoke-song-list.html') r = requests.get('https://www.karafun.de/karaoke-song-list.html')
soup = BeautifulSoup(r.content, 'html.parser') soup = BeautifulSoup(r.content, 'html.parser')
url = soup.findAll('a', href=True, text='Verfügbar in CSV-Format')[0]['href'] url = soup.findAll(
'a', href=True, text='Verfügbar in CSV-Format')[0]['href']
return url return url
def get_songs(url): def get_songs(url):
r = requests.get(url) r = requests.get(url)
return r.text return r.text
def is_valid_uuid(val): def is_valid_uuid(val):
try: try:
uuid.UUID(str(val)) uuid.UUID(str(val))
@ -33,20 +28,14 @@ def is_valid_uuid(val):
except ValueError: except ValueError:
return False return False
def check_config_exists(): def check_config_exists():
eng = database.get_db_engine() return database.check_config_table()
with eng.connect() as conn:
if conn.dialect.has_table(conn, 'config'):
if (conn.execute("SELECT COUNT(*) FROM config").fetchone()[0] > 0): # type: ignore
return True
else:
return False
else:
return False
def load_version(app: Flask): def load_version(app: Flask):
if os.environ.get("SOURCE_VERSION"): if os.environ.get("SOURCE_VERSION"):
app.config['VERSION'] = os.environ.get("SOURCE_VERSION")[0:7] # type: ignore app.config['VERSION'] = os.environ.get("SOURCE_VERSION")[0:7] # type: ignore # noqa: E501
elif os.path.isfile(".version"): elif os.path.isfile(".version"):
with open('.version', 'r') as file: with open('.version', 'r') as file:
data = file.read().replace('\n', '') data = file.read().replace('\n', '')
@ -56,21 +45,22 @@ def load_version(app: Flask):
app.config['VERSION'] = "" app.config['VERSION'] = ""
else: else:
app.config['VERSION'] = "" app.config['VERSION'] = ""
def load_dbconfig(app: Flask): def load_dbconfig(app: Flask):
if os.environ.get("FLASK_ENV") == "development": if os.environ.get("FLASK_ENV") == "development":
app.config['DBCONNSTRING'] = os.environ.get("DBSTRING") app.config['DBCONNSTRING'] = os.environ.get("DBSTRING")
else: else:
if os.environ.get("DEPLOYMENT_PLATFORM") == "Heroku": if os.environ.get("DEPLOYMENT_PLATFORM") == "Heroku":
if os.environ.get("JAWSDB_MARIA_URL"): if os.environ.get("JAWSDB_MARIA_URL"):
app.config['DBCONNSTRING'] = os.environ.get("JAWSDB_MARIA_URL") app.config['DBCONNSTRING'] = os.environ.get("JAWSDB_MARIA_URL")
else: else:
app.config['DBCONNSTRING'] = "" app.config['DBCONNSTRING'] = ""
if os.environ.get("DEPLOYMENT_PLATFORM") == "Docker": if os.environ.get("DEPLOYMENT_PLATFORM") == "Docker":
if os.environ.get("DBSTRING"): if os.environ.get("DBSTRING"):
app.config['DBCONNSTRING'] = os.environ.get("DBSTRING") app.config['DBCONNSTRING'] = os.environ.get("DBSTRING")
else: else:
app.config['DBCONNSTRING'] = "" app.config['DBCONNSTRING'] = ""
elif os.path.isfile(".dbconn"): elif os.path.isfile(".dbconn"):
with open('.dbconn', 'r') as file: with open('.dbconn', 'r') as file:
data = file.read().replace('\n', '') data = file.read().replace('\n', '')
@ -79,25 +69,48 @@ def load_dbconfig(app: Flask):
else: else:
app.config['DBCONNSTRING'] = "" app.config['DBCONNSTRING'] = ""
else: else:
exit("No database connection string found. Cannot continue. Please set the environment variable DBSTRING or create a file .dbconn in the root directory of the project.") exit("""No database connection string found. Cannot continue.
Please set the environment variable DBSTRING or
create a file .dbconn in the root directory of the project.""")
# Check if config exists in DB, if not, create it. # Check if config exists in DB, if not, create it.
def setup_config(app: Flask): def setup_config(app: Flask):
if check_config_exists(): if check_config_exists() is False:
config = database.get_config_list() print("No config found, creating new config")
print("Loaded existing config") initial_username = os.environ.get("INITIAL_USERNAME")
else: initial_password = os.environ.get("INITIAL_PASSWORD")
config = {'username': 'admin', 'password': 'changeme', 'entryquota': 3, 'maxqueue': 20, 'entries_allowed': 1, 'theme': 'default'} if initial_username is None:
for key, value in config.items(): print(
"No initial username set. Please set the environment variable INITIAL_USERNAME")
exit()
if initial_password is None:
print(
"No initial password set. Please set the environment variable INITIAL_PASSWORD")
exit()
default_config = {'username': initial_username,
'password': initial_password,
'entryquota': 3,
'maxqueue': 20,
'entries_allowed': 1,
'theme': 'default.css'}
for key, value in default_config.items():
database.set_config(key, value) database.set_config(key, value)
print("Created new config") print("Created new config")
database.init_event_id()
config = database.get_config_list()
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']
app.config['ENTRY_QUOTA'] = config['entryquota'] app.config['ENTRY_QUOTA'] = config['entryquota']
app.config['MAX_QUEUE'] = config['maxqueue'] app.config['MAX_QUEUE'] = config['maxqueue']
app.config['ENTRIES_ALLOWED'] = bool(config['entries_allowed']) app.config['ENTRIES_ALLOWED'] = bool(config['entries_allowed'])
app.config['THEME'] = config['theme']
app.config['EVENT_ID'] = database.get_event_id()
# set queue admittance # set queue admittance
def set_accept_entries(app: Flask, allowed: bool): def set_accept_entries(app: Flask, allowed: bool):
if allowed: if allowed:
app.config['ENTRIES_ALLOWED'] = True app.config['ENTRIES_ALLOWED'] = True
@ -107,6 +120,8 @@ def set_accept_entries(app: Flask, allowed: bool):
database.set_config('entries_allowed', '0') database.set_config('entries_allowed', '0')
# get queue admittance # get queue admittance
def get_accept_entries(app: Flask) -> bool: def get_accept_entries(app: Flask) -> bool:
state = bool(int(database.get_config('entries_allowed'))) state = bool(int(database.get_config('entries_allowed')))
app.config['ENTRIES_ALLOWED'] = state app.config['ENTRIES_ALLOWED'] = state
@ -115,11 +130,14 @@ def get_accept_entries(app: Flask) -> bool:
# Write settings from current app.config to DB # Write settings from current app.config to DB
def persist_config(app: Flask): def persist_config(app: Flask):
config = {'username': app.config['BASIC_AUTH_USERNAME'], 'password': app.config['BASIC_AUTH_PASSWORD'], 'entryquota': app.config['ENTRY_QUOTA'], 'maxqueue': app.config['MAX_QUEUE']} config = {'username': app.config['BASIC_AUTH_USERNAME'], 'password': app.config['BASIC_AUTH_PASSWORD'],
'entryquota': app.config['ENTRY_QUOTA'], 'maxqueue': app.config['MAX_QUEUE']}
for key, value in config.items(): for key, value in config.items():
database.set_config(key, value) database.set_config(key, value)
# Get available themes from themes directory # Get available themes from themes directory
def get_themes(): def get_themes():
themes = [] themes = []
for theme in os.listdir('./static/css/themes'): for theme in os.listdir('./static/css/themes'):
@ -127,6 +145,8 @@ def get_themes():
return themes return themes
# Set theme # Set theme
def set_theme(app: Flask, theme: str): def set_theme(app: Flask, theme: str):
if theme in get_themes(): if theme in get_themes():
app.config['THEME'] = theme app.config['THEME'] = theme
@ -135,6 +155,15 @@ def set_theme(app: Flask, theme: str):
print("Theme not found, not setting theme.") print("Theme not found, not setting theme.")
def get_current_event_id(app: Flask):
return app.config['EVENT_ID']
def reset_current_event_id(app: Flask):
database.reset_event_id()
app.config['EVENT_ID'] = database.get_event_id()
def nocache(view): def nocache(view):
@wraps(view) @wraps(view)
def no_cache(*args, **kwargs): def no_cache(*args, **kwargs):
@ -144,5 +173,5 @@ def nocache(view):
response.headers['Pragma'] = 'no-cache' response.headers['Pragma'] = 'no-cache'
response.headers['Expires'] = '-1' response.headers['Expires'] = '-1'
return response return response
return update_wrapper(no_cache, view) return update_wrapper(no_cache, view)

View File

@ -1,4 +1,4 @@
from app import app from app import app
if __name__ == "__main__": if __name__ == "__main__":
app.run() app.run()

View File

@ -1,7 +1,36 @@
requests autopep8==2.0.2
pandas beautifulsoup4==4.12.0
Flask-BasicAuth bs4==0.0.1
bs4 certifi==2022.12.7
gunicorn charset-normalizer==3.1.0
SQLAlchemy click==8.1.3
mysqlclient flake8==6.0.0
Flask==2.2.3
Flask-BasicAuth==0.2.0
greenlet==2.0.2
gunicorn==20.1.0
idna==3.4
itsdangerous==2.1.2
Jinja2==3.1.2
mariadb==1.1.6
MarkupSafe==2.1.2
mccabe==0.7.0
mysql==0.0.3
mysqlclient==2.1.1
numpy==1.24.2
packaging==23.0
pandas==1.5.3
pycodestyle==2.10.0
pyflakes==3.0.1
PyMySQL==1.0.3
python-dateutil==2.8.2
pytz==2023.3
requests==2.28.2
six==1.16.0
soupsieve==2.4
SQLAlchemy==2.0.7
toml==0.10.2
tomli==2.0.1
typing_extensions==4.5.0
urllib3==1.26.15
Werkzeug==2.2.3

View File

@ -1,133 +1,201 @@
body { :root {
padding-top: 5rem; /* Navbar */
--navbar-background-color: #343a40;
--navbar-text-color: rgba(255, 255, 255, .5);
--navbar-text-color-hover: rgba(255, 255, 255, .75);
--navbar-text-color-active: rgba(255, 255, 255, 1);
/* Common */
--background-color: #ffffff;
--background-color-var: #f5f5f5;
--text-color: #212529;
--text-color-var: #343a40;
/* Modals */
--modal-background-color: #ffffff;
--modal-separator-color: #dee2e6;
--modal-close-color: #212529;
/* Tables */
--table-border-color: #dee2e6;
/* Input */
--input-background-color: #ffffff;
/* Misc */
--copy-highlight-color: rgba(251, 255, 0, 0.6);
} }
html, body {
height: 100%; body {
padding-top: 5rem;
background-color: var(--background-color);
}
html,
body {
height: 100%;
} }
.site { .site {
height: auto; height: auto;
min-height: 100%; min-height: 100%;
} }
main { main {
padding-bottom: 60px; /* Höhe des Footers */ padding-bottom: 60px;
/* Höhe des Footers */
} }
.footer { .footer {
margin-top: -60px; margin-top: -60px;
width: 100%; width: 100%;
height: 60px; height: 60px;
/* Set the fixed height of the footer here */ /* Set the fixed height of the footer here */
/*line-height: 60px; /* Vertically center the text there */ /*line-height: 60px; /* Vertically center the text there */
background-color: #f5f5f5; background-color: var(--background-color-var);
} }
.topbutton { .topbutton {
width: 100%; width: 100%;
} }
table td { table td {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
table.entries tbody tr[data-index="0"] { table.entries tbody tr[data-index="0"] {
background-color: #007bff80; background-color: #007bff80;
font-weight: 600; font-weight: 600;
} }
table.entries tbody tr[data-index="1"] { table.entries tbody tr[data-index="1"] {
background-color: #007bff40; background-color: #007bff40;
font-weight: 500; font-weight: 500;
} }
table.entries tbody tr[data-index="2"] { table.entries tbody tr[data-index="2"] {
background-color: #007bff20; background-color: #007bff20;
font-weight: 400; font-weight: 400;
} }
table.entries tbody tr[data-index="3"] { table.entries tbody tr[data-index="3"] {
background-color: #007bff10; background-color: #007bff10;
} }
table td:first-child { table td:first-child {
max-width: 200px !important; max-width: 200px !important;
}
.fa-solid {
vertical-align: auto;
} }
@media (min-width: 768px) { @media (min-width: 768px) {
.topbutton { .topbutton {
width: auto; width: auto;
} }
} }
@media print { @media print {
body { body {
font-size: 1.3em; font-size: 1.3em;
} }
.footer { .footer {
display: none !important; display: none !important;
} }
.admincontrols { .admincontrols {
display: none; display: none;
} }
}
body {
background-color: var(--background-color);
color: var(--text-color);
}
.footer {
background-color: var(--background-color-var);
}
.modal-content {
background-color: var(--background-color);
color: var(--text-color);
}
.modal-header {
background-color: var(--background-color);
color: var(--text-color-var);
border-color: var(var(--modal-separator-color));
}
.modal-footer {
background-color: var(--background-color);
color: var(--text-color-var);
border-color: var(var(--modal-separator-color));
}
.form-control {
background-color: var(--input-background-color);
color: var(--text-color)
}
.form-control:focus {
background-color: var(--input-background-color);
color: var(--text-color)
}
.table td,
.table th {
border-color: var(--table-border-color)
}
.table thead th {
border-color: var(--table-border-color)
}
table td.buttoncell {
text-align: end;
}
.close {
color: var(--text-color)
}
pre {
color: var(--text-color-var)
}
#entrytable td>span:hover {
background-color: var(--copy-highlight-color);
} }
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
:root {
/* Navbar */
--navbar-background-color: #343a40;
--navbar-text-color: rgba(255, 255, 255, .5);
--navbar-text-color-hover: rgba(255, 255, 255, .75);
--navbar-text-color-active: rgba(255, 255, 255, 1);
body { /* Common */
background-color: #121212; --background-color: #121212;
color: #f5f5f5; --background-color-var: #232323;
} --text-color: #f5f5f5;
--text-color-var: #a2a2a2;
.footer {
background-color: #232323;
}
.modal-content { /* Modals */
background-color: #121212; --modal-background-color: #121212;
color: #f5f5f5; --modal-separator-color: #232323;
} --modal-close-color: #f5f5f5;
.modal-header { /* Tables */
background-color: #121212; --table-border-color: #232323;
color: #f5f5f5;
border-color: #232323;
}
.modal-footer {
background-color: #121212;
color: #f5f5f5;
border-color: #232323;
}
.form-control {
background-color: #343434;
color: #f5f5f5;
}
.form-control:focus {
background-color: #343434;
color: #f5f5f5;
}
.table td, .table th {
border-color: #232323;
}
.table thead th {
border-color: #232323;
}
.close {
color: #f5f5f5;
}
pre {
color: #a2a2a2;
}
/* Input */
--input-background-color: #343434;
}
} }

View File

@ -6,6 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content=""> <meta name="description" content="">
<meta name="author" content=""> <meta name="author" content="">
<meta name="color-scheme" content="light dark" />
<link rel="icon" href="favicon.ico"> <link rel="icon" href="favicon.ico">
<link rel="manifest" href="/static/manifest.webmanifest"> <link rel="manifest" href="/static/manifest.webmanifest">
@ -14,16 +15,20 @@
<!-- Bootstrap-Tables --> <!-- Bootstrap-Tables -->
<link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.21.2/dist/bootstrap-table.min.css"> <link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.21.2/dist/bootstrap-table.min.css">
<!-- Bootstrap-Toaster-->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-toaster/4.1.2/css/bootstrap-toaster.min.css"
integrity="sha512-kYPLvO+Bu+xttOhbQvxs9nx7XSdxrb2JexRxQ3CpJQ7EtmlkBsWyOjlinLgiLWeLxuupFYB4cPqLOo0gnBnzeQ==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<!-- 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://cdn.jsdelivr.net/npm/bootstrap@4.2.1/dist/css/bootstrap.min.css" integrity="sha384-GJzZqFGwb1QTTN6wy59ffF1BuGJpLSa9DkKMp0DgiMDm4iYMj70gZWKYbI706tWS" crossorigin="anonymous">
integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
<!-- 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://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
integrity="sha384-oS3vJWv+0UjzBfQzYUhtDYW+Pj2yciDJxpsK1OYPAYjqT085Qq/1cq5FLXAZQ7Ay" crossorigin="anonymous"> integrity="sha512-iecdLmaskl7CVkqkXNQ/ZH/XLlvWZOJyj7Yy7tcenmpD1ypASozpmT/E0iPtmFIB46ZmdtAc9eNBvH0H/ZpiBw=="
crossorigin="anonymous" referrerpolicy="no-referrer" />
<!-- Bootstraptoggle --> <!-- Bootstraptoggle -->
<link href="https://gitcdn.github.io/bootstrap-toggle/2.2.2/css/bootstrap-toggle.min.css" rel="stylesheet"> <link href="https://gitcdn.github.io/bootstrap-toggle/2.2.2/css/bootstrap-toggle.min.css" rel="stylesheet">
@ -86,16 +91,12 @@
<!-- Bootstrap core JavaScript <!-- Bootstrap core JavaScript
================================================== --> ================================================== -->
<!-- Placed at the end of the document so the pages load faster --> <!-- Placed at the end of the document so the pages load faster -->
<script src="https://code.jquery.com/jquery-3.2.1.min.js" <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.4/jquery.min.js"
integrity="sha384-xBuQ/xzmlsLoJpyjoggmTEz8OWUFM0/RC5BsqQBDX2v5cMvDHcMakNTNrHIW2I5f" crossorigin="anonymous"> integrity="sha512-pumBsjNRGGqkPzKHndZMaAG+bir374sORyzM3uulLV14lN5LyykqNk8eEeUlUkB3U0M4FApyaHraT65ihJhDpQ=="
</script> crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" <script src="https://cdn.jsdelivr.net/npm/popper.js@1.14.6/dist/umd/popper.min.js" integrity="sha384-wHAiFfRlMFy6i5SRaxvfOCifBUQy1xHdJ/yoi7FRNXMRBu5WHdZYu1hA6ZOblgut" crossorigin="anonymous"></script>
integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"> <script src="https://cdn.jsdelivr.net/npm/bootstrap@4.2.1/dist/js/bootstrap.min.js" integrity="sha384-B0UglyR+jN6CkvvICOB2joaf5I4l3gm9GU6Hc1og6Ls7i6U/mkkaduKaBhlAXv9k" crossorigin="anonymous"></script>
</script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js"
integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous">
</script>
<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>
@ -103,10 +104,13 @@
<script <script
src="https://unpkg.com/bootstrap-table@1.21.2/dist/extensions/auto-refresh/bootstrap-table-auto-refresh.min.js"></script> src="https://unpkg.com/bootstrap-table@1.21.2/dist/extensions/auto-refresh/bootstrap-table-auto-refresh.min.js"></script>
<script src="https://gitcdn.github.io/bootstrap-toggle/2.2.2/js/bootstrap-toggle.min.js"></script> <script src="https://gitcdn.github.io/bootstrap-toggle/2.2.2/js/bootstrap-toggle.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-toaster/4.1.2/js/bootstrap-toaster.min.js"
integrity="sha512-Ur6jgeoP3jnn38C7oBzDqMLRb+wxG2PXLKqgx2vgQ1ePFvbJ28f9iQSJplHD0APFHELOeS/df+RPNeENFtLrYw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
{% block extrajs %}{% endblock %} {% block extrajs %}{% endblock %}
<script> <script>
$(document).ready(function () { $(document).ready(function () {
loadOrGenerateClientId() checkEventID()
// get current URL path and assign 'active' class // get current URL path and assign 'active' class
var pathname = window.location.pathname; var pathname = window.location.pathname;
$('.navbar-nav > li > a[href="' + pathname + '"]').parent().addClass('active'); $('.navbar-nav > li > a[href="' + pathname + '"]').parent().addClass('active');
@ -125,9 +129,53 @@
function loadOrGenerateClientId() { function loadOrGenerateClientId() {
if (!localStorage.getItem("clientId")) { if (!localStorage.getItem("clientId")) {
localStorage.setItem("clientId",create_UUID()) localStorage.setItem("clientId", create_UUID())
} }
} }
function getClientId() {
return localStorage.getItem("clientId")
}
async function checkEventID() {
const localEventID = localStorage.getItem("eventID")
const resp = await fetch("/api/events/current")
const respJson = await resp.json()
const remoteEventID = respJson.event
if (localEventID == null || localEventID != remoteEventID) {
localStorage.clear()
localStorage.setItem("eventID", remoteEventID)
loadOrGenerateClientId()
}
}
function addEntry(entryId) {
entryArray = JSON.parse(localStorage.getItem("ownedEntries"))
if (entryArray == null) {
entryArray = []
}
entryArray.push(entryId)
localStorage.setItem("ownedEntries", JSON.stringify(entryArray))
}
function removeEntry(entryId) {
entryArray = JSON.parse(localStorage.getItem("ownedEntries"))
if (entryArray == null) {
entryArray = []
}
entryArray = entryArray.filter(function(value, index, arr){ return value != entryId;});
localStorage.setItem("ownedEntries", JSON.stringify(entryArray))
}
function getOwnedEntries() {
var entries = JSON.parse(localStorage.getItem("ownedEntries"))
if (entries == null) {
entries = []
}
return entries;
}
</script> </script>
</body> </body>

View File

@ -17,6 +17,7 @@
<th data-field="Name">Name</th> <th data-field="Name">Name</th>
<th data-field="Title">Song</th> <th data-field="Title">Song</th>
<th data-field="Artist">Künstler</th> <th data-field="Artist">Künstler</th>
<th scope="col" data-formatter="TableActionsFormatter"></th>
</tr> </tr>
</thead> </thead>
</table> </table>
@ -29,7 +30,48 @@ $.getJSON("/api/entries/accept", (data) => {
$("#bfb").addClass("disabled") $("#bfb").addClass("disabled")
$("#bfb").prop("aria-disabled",true); $("#bfb").prop("aria-disabled",true);
$("#bfb").prop("tabindex","-1"); $("#bfb").prop("tabindex","-1");
$("#bfb").wrap("<span class='tooltip-span' tabindex='0' data-toggle='tooltip' data-placement='bottom'></span>");
$(".tooltip-span").prop("title", "Eintragungen sind leider momentan nicht möglich.")
$('[data-toggle="tooltip"]').tooltip()
} }
}) })
function TableActionsFormatter(value,row,index) {
console.log("Value: " + value + ", Row: " + row + ", Index: " + index)
console.log(row)
if (getOwnedEntries().includes(row.entry_ID)) {
return "<button type='button' class='btn btn-danger' data-toggle='tooltip' data-placement='top' title='Eintrag zurückziehen' onclick=\"event.stopPropagation();$(this).tooltip('dispose');requestDeletionAsUser("+row["entry_ID"]+")\"><i class='fas fa-trash'></i></button>"
}
return ""
}
function requestDeletionAsUser(id) {
bootbox.confirm("Wirklich den Eintrag zurückziehen? Das könnte zu einer langen Wartezeit führen!", function (result) {
if (result) {
payload = {
"client_id": localStorage.getItem("clientId"),
"entry_id": id
}
$.ajax({
url: "/api/entries/delete/"+id,
type: "POST",
data: JSON.stringify(payload),
contentType: "application/json; charset=utf-8",
dataType: "json",
success: function(result) {
toast = {
title: "Erfolgreich zurückgezogen",
message: "Eintrag wurde gelöscht",
status: TOAST_STATUS.SUCCESS,
timeout: 5000
}
Toast.create(toast);
location.reload()
}
})
}
})
}
</script> </script>
{% endblock %} {% endblock %}

View File

@ -1,15 +1,13 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block title %}Warteliste-Admin{% endblock %} {% block title %}Warteliste-Admin{% endblock %}
{% block content %} {% block content %}
<style> <style>
table td:nth-child(2) { table td:nth-child(2) {
overflow-y: hidden; overflow-y: hidden;
overflow-x: auto; overflow-x: auto;
text-overflow: clip; text-overflow: clip;
max-width: 200px !important; max-width: 200px !important;
} }
</style> </style>
<div class="container"> <div class="container">
<div id="toolbar"> <div id="toolbar">
@ -18,30 +16,19 @@ table td:nth-child(2) {
<button type="button" class="topbutton btn btn-danger" onclick="confirmUpdateSongDatabase()"><i <button type="button" class="topbutton btn btn-danger" onclick="confirmUpdateSongDatabase()"><i
class="fas fa-file-import mr-2"></i>Song-Datenbank class="fas fa-file-import mr-2"></i>Song-Datenbank
aktualisieren</button> aktualisieren</button>
<input id="entryToggle" type="checkbox" class="topbutton" data-toggle="toggle" data-on="Eintragen erlaubt" data-off="Eintragen deaktiviert" data-onstyle="success" data-offstyle="danger"> <input id="entryToggle" type="checkbox" class="topbutton" data-toggle="toggle" data-on="Eintragen erlaubt"
data-off="Eintragen deaktiviert" data-onstyle="success" data-offstyle="danger">
</div> </div>
<table class="table entries" <table class="table entries" id="entrytable" data-toggle="table" data-search="true" data-show-columns="true"
id="entrytable" data-show-toggle="true" data-multiple-select-row="true" data-click-to-select="true" data-toolbar="#toolbar"
data-toggle="table" data-pagination="true" data-show-extended-pagination="true" data-classes="table table-hover"
data-search="true" data-url="/api/queue" data-show-refresh="true" data-auto-refresh="true" data-auto-refresh-interval="10">
data-show-columns="true"
data-show-toggle="true"
data-multiple-select-row="true"
data-click-to-select="true"
data-toolbar="#toolbar"
data-pagination="true"
data-show-extended-pagination="true"
data-classes="table table-hover"
data-url="/api/queue"
data-show-refresh="true"
data-auto-refresh="true"
data-auto-refresh-interval="10">
<thead> <thead>
<tr> <tr>
<th data-field="state" data-checkbox="true"></th> <th data-field="state" data-checkbox="true"></th>
<th scope="col" data-field="Name">Name</th> <th scope="col" data-field="Name" data-formatter="CopyFormatter">Name</th>
<th scope="col" data-field="Title">Song</th> <th scope="col" data-field="Title"data-formatter="CopyFormatter">Song</th>
<th scope="col" data-field="Artist">Künstler</th> <th scope="col" data-field="Artist"data-formatter="CopyFormatter">Künstler</th>
<th scope="col" data-formatter="TableActions">Aktionen</th> <th scope="col" data-formatter="TableActions">Aktionen</th>
</tr> </tr>
</thead> </thead>
@ -53,15 +40,18 @@ table td:nth-child(2) {
<script> <script>
$(function () { $(function () {
refreshEntryToggle() refreshEntryToggle()
$('#entryToggle').change(function() { $('#entryToggle').change(function () {
$.ajax({url: "/api/entries/accept/"+($('#entryToggle').is(":checked") ? "1" : "0"), complete: setTimeout(refreshEntryToggle, 1000)}); $.ajax({ url: "/api/entries/accept/" + ($('#entryToggle').is(":checked") ? "1" : "0"), complete: setTimeout(refreshEntryToggle, 1000) });
}) })
$("#entrytable").bootstrapTable().on('load-success.bs.table', function() { $("#entrytable").bootstrapTable().on('load-success.bs.table', function () {
$('[data-toggle="tooltip"]').tooltip() $('[data-toggle="tooltip"]').tooltip()
}) })
$('[data-toggle="tooltip"]').tooltip({
trigger: 'hover'
})
}) })
function confirmDeleteEntry(name, entry_id) { function confirmDeleteEntry(name, entry_id) {
bootbox.confirm("Wirklich den Eintrag von "+name+" löschen?", function(result){ bootbox.confirm("Wirklich den Eintrag von " + name + " löschen?", function (result) {
if (result) { if (result) {
deleteEntry(entry_id) deleteEntry(entry_id)
} }
@ -69,7 +59,7 @@ table td:nth-child(2) {
} }
function confirmDeleteSelectedEntries() { function confirmDeleteSelectedEntries() {
bootbox.confirm({ bootbox.confirm({
message: "Wirklich gewählte Eintragungen löschen?", message: "Wirklich gewählte Eintragungen löschen?",
buttons: { buttons: {
confirm: { confirm: {
label: 'Ja', label: 'Ja',
@ -80,7 +70,7 @@ table td:nth-child(2) {
className: 'btn btn-secondary' className: 'btn btn-secondary'
} }
}, },
callback: function(result){ callback: function (result) {
if (result) { if (result) {
DeleteSelectedEntries(getIdSelections()) DeleteSelectedEntries(getIdSelections())
} }
@ -100,22 +90,22 @@ table td:nth-child(2) {
className: 'btn btn-secondary' className: 'btn btn-secondary'
} }
}, },
callback: function(result){ callback: function (result) {
if (result) { if (result) {
var dialog = bootbox.dialog({ var dialog = bootbox.dialog({
message: '<p class="text-center mb-0"><i class="fa fa-spin fa-cog"></i> Aktualisiere Song-Datenbank...</p>', message: '<p class="text-center mb-0"><i class="fa fa-spin fa-cog"></i> Aktualisiere Song-Datenbank...</p>',
closeButton: false closeButton: false
}); });
updateSongDatabase(dialog) updateSongDatabase(dialog)
} }
} }
}) })
} }
function refreshEntryToggle() { function refreshEntryToggle() {
$.getJSON("/api/entries/accept", (data) => { $.getJSON("/api/entries/accept", (data) => {
if (data["value"]!=$('#entryToggle').is(":checked")) { if (data["value"] != $('#entryToggle').is(":checked")) {
if(data["value"]==1) { if (data["value"] == 1) {
$('#entryToggle').data('bs.toggle').on('true') $('#entryToggle').data('bs.toggle').on('true')
} }
else { else {
@ -125,53 +115,53 @@ table td:nth-child(2) {
}) })
} }
function deleteEntry(entry_id) { function deleteEntry(entry_id) {
$.ajax({ $.ajax({
type: 'GET', type: 'GET',
url: '/api/entries/delete/'+entry_id, url: '/api/entries/delete/' + entry_id,
contentType: "application/json", contentType: "application/json",
dataType: 'json', dataType: 'json',
async: false async: false
}); });
$("#entrytable").bootstrapTable('refresh') $("#entrytable").bootstrapTable('refresh')
} }
function markEntryAsSung(entry_id) { function markEntryAsSung(entry_id) {
$.ajax({ $.ajax({
type: 'GET', type: 'GET',
url: '/api/entries/mark_sung/'+entry_id, url: '/api/entries/mark_sung/' + entry_id,
contentType: "application/json", contentType: "application/json",
dataType: 'json', dataType: 'json',
async: false async: false
}); });
$("#entrytable").bootstrapTable('refresh') $("#entrytable").bootstrapTable('refresh')
} }
function markEntryAsTransferred(entry_id) { function markEntryAsTransferred(entry_id) {
$.ajax({ $.ajax({
type: 'GET', type: 'GET',
url: '/api/entries/mark_transferred/'+entry_id, url: '/api/entries/mark_transferred/' + entry_id,
contentType: "application/json", contentType: "application/json",
dataType: 'json', dataType: 'json',
async: false async: false
}); });
$("#entrytable").bootstrapTable('refresh') $("#entrytable").bootstrapTable('refresh')
} }
function DeleteSelectedEntries(ids) { function DeleteSelectedEntries(ids) {
$.ajax({ $.ajax({
type: 'POST', type: 'POST',
url: '/api/entries/delete', url: '/api/entries/delete',
data: JSON.stringify(ids), // or JSON.stringify ({name: 'jonas'}), data: JSON.stringify(ids), // or JSON.stringify ({name: 'jonas'}),
error: function() { error: function () {
bootbox.alert({ bootbox.alert({
message: "Fehler beim Löschen der Eintragungen.", message: "Fehler beim Löschen der Eintragungen.",
}) })
}, },
success: function() { success: function () {
$("#entrytable").bootstrapTable('refresh') $("#entrytable").bootstrapTable('refresh')
}, },
contentType: "application/json", contentType: "application/json",
dataType: 'json' dataType: 'json'
@ -183,32 +173,48 @@ table td:nth-child(2) {
url: '/api/songs/update', url: '/api/songs/update',
contentType: "application/json", contentType: "application/json",
dataType: 'json', dataType: 'json',
success: function(data) { success: function (data) {
wait_dialog.modal('hide') wait_dialog.modal('hide')
bootbox.alert({ bootbox.alert({
message: data["status"], message: data["status"],
callback: function() { callback: function () {
$("#entrytable").bootstrapTable('refresh') $("#entrytable").bootstrapTable('refresh')
} }
}) })
} }
}); });
} }
function TableActions (value, row, index) { function TableActions(value, row, index) {
console.log("Value: "+value+", Row: "+row+", Index: "+index)
console.log(row)
let outerHTML = "" let outerHTML = ""
if (row.Transferred==1) { if (row.Transferred == 1) {
outerHTML = "<button type=\"button\" class=\"btn btn-default\" onclick=\"markEntryAsTransferred("+row.entry_ID+")\"><i class=\"fas fa-backward\"></i></button>&nbsp;<button type=\"button\" class=\"btn btn-success\" data-toggle=\"tooltip\" data-placement=\"top\" title=\"Als gesungen markieren\" onclick=\"markEntryAsSung("+row.entry_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.entry_ID+")\"><i class=\"fas fa-trash\"></i></button>"; outerHTML = "<button type=\"button\" class=\"btn btn-default\" data-toggle=\"tooltip\" data-placement=\"top\" title=\"Markierung zurückziehen\" onclick=\"event.stopPropagation();$(this).tooltip('dispose');markEntryAsTransferred(" + row.entry_ID + ")\"><i class=\"fas fa-backward\"></i></button>&nbsp;<button type=\"button\" class=\"btn btn-success\" data-toggle=\"tooltip\" data-placement=\"top\" title=\"Als gesungen markieren\" onclick=\"event.stopPropagation();$(this).tooltip('dispose');markEntryAsSung(" + row.entry_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=\"event.stopPropagation();$(this).tooltip('dispose');confirmDeleteEntry(this.parentElement.parentElement.children[1].innerText," + row.entry_ID + ")\"><i class=\"fas fa-trash\"></i></button>";
} else { } else {
outerHTML = "<button type=\"button\" class=\"btn btn-info\" onclick=\"markEntryAsTransferred("+row.entry_ID+")\"><i class=\"fas fa-exchange-alt\"></i></button>&nbsp;<button type=\"button\" class=\"btn btn-success\" data-toggle=\"tooltip\" data-placement=\"top\" title=\"Als gesungen markieren\" onclick=\"markEntryAsSung("+row.entry_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.entry_ID+")\"><i class=\"fas fa-trash\"></i></button>"; outerHTML = "<button type=\"button\" class=\"btn btn-info\" data-toggle=\"tooltip\" data-placement=\"top\" title=\"Als übertragen markieren\" onclick=\"event.stopPropagation();$(this).tooltip('dispose');markEntryAsTransferred(" + row.entry_ID + ")\"><i class=\"fas fa-exchange-alt\"></i></button>&nbsp;<button type=\"button\" class=\"btn btn-success\" data-toggle=\"tooltip\" data-placement=\"top\" title=\"Als gesungen markieren\" onclick=\"event.stopPropagation();$(this).tooltip('dispose');markEntryAsSung(" + row.entry_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=\"event.stopPropagation();$(this).tooltip('dispose');confirmDeleteEntry(this.parentElement.parentElement.children[1].innerText," + row.entry_ID + ")\"><i class=\"fas fa-trash\"></i></button>";
} }
return outerHTML; return outerHTML;
} }
function CopyFormatter(value, row, index) {
let escapedString = value.replace("\"","\\\"").replace("\'", "\\\'")
return "<span onclick='copyAndNotify(this.innerText)'>"+value+"</span>";
}
function getIdSelections() { function getIdSelections() {
return $.map($("#entrytable").bootstrapTable('getSelections'), function (row) { return $.map($("#entrytable").bootstrapTable('getSelections'), function (row) {
return row.entry_ID return row.entry_ID
}) })
} }
function copyAndNotify(text) {
navigator.clipboard.writeText(text).then(function () {
let toast = {
title: "Kopiert",
message: text,
status: 1,
timeout: 5000
};
Toast.create(toast);
})
}
</script> </script>
{% endblock %} {% endblock %}

View File

@ -18,6 +18,18 @@
{% endfor %} {% endfor %}
</select> </select>
</p> </p>
<div class="alert alert-warning" role="alert">
<i class="fas fa-exclamation-triangle mr-1"></i>
<strong>Warnung:</strong> Änderungen an den folgenden Einstellungen führen zu einer sofortigen Abmeldung!
</div>
<p>
<label for="username">Benutzername</label>
<input type="text" class="form-control" id="username" name="username" value={{app.config['BASIC_AUTH_USERNAME']}}>
</p>
<p>
<label for="password">Passwort ändern</label>
<input type="password" class="form-control" id="password" name="password">
</p>
<input type="submit" class="btn btn-primary mr-1 mb-2" value="Einstellungen anwenden"> <input type="submit" class="btn btn-primary mr-1 mb-2" value="Einstellungen anwenden">
</form> </form>
<details> <details>

View File

@ -67,6 +67,9 @@
e.preventDefault(); e.preventDefault();
submitModal(); submitModal();
}); });
$('#enqueueModal').on('shown.bs.modal', function (e) {
$("#singerNameInput").focus();
})
}); });
@ -79,7 +82,7 @@
$.ajax({ $.ajax({
type: 'POST', type: 'POST',
url: '/api/enqueue', url: '/api/enqueue',
data: JSON.stringify(data), // or JSON.stringify ({name: 'jonas'}), data: JSON.stringify(data),
success: success_callback, success: success_callback,
statusCode: { statusCode: {
423: blocked_callback 423: blocked_callback
@ -96,7 +99,18 @@
function submitModal() { function submitModal() {
var name = $("#singerNameInput").val(); var name = $("#singerNameInput").val();
var id = $("#selectedId").attr("value"); var id = $("#selectedId").attr("value");
enqueue(localStorage.getItem("clientId"),id, name, function () { enqueue(localStorage.getItem("clientId"),id, name, function (response) {
console.log(response);
entryID = response["entry_id"];
toast = {
title: "Erfolgreich eingetragen",
message: "Du wurdest erfolgreich eingetragen.",
status: TOAST_STATUS.SUCCESS,
timeout: 5000
}
Toast.create(toast);
console.log("Entry ID: " + entryID);
addEntry(entryID);
$("#enqueueModal").modal('hide'); $("#enqueueModal").modal('hide');
window.location.href = '/#end'; window.location.href = '/#end';
}, function (response) { }, function (response) {
@ -118,7 +132,7 @@
$(".enqueueButton").prop("disabled", true) $(".enqueueButton").prop("disabled", true)
$(".enqueueButton").prop("style", "pointer-events: none;") $(".enqueueButton").prop("style", "pointer-events: none;")
$(".enqueueButton").wrap("<span class='tooltip-span' tabindex='0' data-toggle='tooltip' data-placement='top'></span>"); $(".enqueueButton").wrap("<span class='tooltip-span' tabindex='0' data-toggle='tooltip' data-placement='top'></span>");
$(".tooltip-span").prop("title", "Eintragungen sind leider nicht mehr möglich.") $(".tooltip-span").prop("title", "Eintragungen sind leider momentan nicht möglich.")
$('[data-toggle="tooltip"]').tooltip() $('[data-toggle="tooltip"]').tooltip()
} else { } else {
$(".enqueueButton").prop("disabled", false) $(".enqueueButton").prop("disabled", false)

View File

@ -6,7 +6,7 @@ secrets:
services: services:
karaoqueue: karaoqueue:
image: "phillipkhne/karaoqueue:latest" image: "ghcr.io/phoenixtwofive/karaoqueue:v2023.04.1"
build: . build: .
restart: always restart: always
ports: ports:
@ -15,4 +15,9 @@ services:
db: db:
image: mariadb image: mariadb
restart: always restart: always
env_file: .env env_file: .env
volumes:
- karaoqueue-db:/var/lib/mysql
volumes:
karaoqueue-db: