diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..35ed8c9 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,33 @@ +{ + "image": "mcr.microsoft.com/devcontainers/universal:2", + "hostRequirements": { + "cpus": 4 + }, + "waitFor": "onCreateCommand", + "onCreateCommand": "curl -LsS https://r.mariadb.com/downloads/mariadb_repo_setup | sudo bash && sudo apt install -y libmariadb3 libmariadb-dev", + "updateContentCommand": "pip install -r backend/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] + } + \ No newline at end of file diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..eabd9a7 --- /dev/null +++ b/.editorconfig @@ -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 \ No newline at end of file diff --git a/.env.dev b/.env.dev new file mode 100644 index 0000000..df90e9c --- /dev/null +++ b/.env.dev @@ -0,0 +1,12 @@ +# MariaDB +MARIADB_ROOT_PASSWORD=mariadb_root_password +MARIADB_ROOT_HOST=localhost +MARIADB_DATABASE=karaoqueue +MARIADB_USER=karaoqueue +MARIADB_PASSWORD=mariadb_karaoqueue_password + +# Karaoqueue +DEPLOYMENT_PLATFORM=Docker +DBSTRING="mysql+pymysql://karaoqueue:mariadb_karaoqueue_password@127.0.0.1:3306/karaoqueue?charset=utf8mb4" +INITIAL_USERNAME=admin +INITIAL_PASSWORD=changeme \ No newline at end of file diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..1cfd4e0 --- /dev/null +++ b/.flake8 @@ -0,0 +1,3 @@ +[flake8] +ignore = E501 +max-line-length = 120 \ No newline at end of file diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml new file mode 100644 index 0000000..6812373 --- /dev/null +++ b/.github/workflows/lint.yaml @@ -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 }} diff --git a/.gitignore b/.gitignore index aba13c6..7548dd0 100644 --- a/.gitignore +++ b/.gitignore @@ -87,7 +87,7 @@ ipython_config.py # 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 # install all needed dependencies. -#Pipfile.lock +Pipfile.lock # celery beat schedule file celerybeat-schedule @@ -139,4 +139,7 @@ node_modules/ # Version identification file .version -**/.angular/ \ No newline at end of file +**/.angular/ + +# Docker secrets +secrets.yml diff --git a/.vscode/launch.json b/.vscode/launch.json index 2e00725..594135a 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,7 +5,7 @@ "version": "0.2.0", "configurations": [ { - "preLaunchTask": "versiondump", + "preLaunchTask": "mariadb", "name": "Python: Flask", "type": "python", "cwd": "${workspaceFolder}/backend", @@ -14,8 +14,9 @@ "env": { "FLASK_APP": "backend/app.py", "FLASK_ENV": "development", - "FLASK_DEBUG": "1" + "FLASK_DEBUG": "1", }, + "envFile": "${workspaceFolder}/.env.dev", "args": [ "run", "--no-debugger", @@ -24,7 +25,7 @@ "jinja": true }, { - "preLaunchTask": "versiondump", + "preLaunchTask": "mariadb", "name": "Python: Flask (with reload)", "type": "python", "cwd": "${workspaceFolder}/backend", @@ -35,6 +36,7 @@ "FLASK_ENV": "development", "FLASK_DEBUG": "1" }, + "envFile": "${workspaceFolder}/.env.dev", "args": [ "run", "--no-debugger" @@ -42,7 +44,7 @@ "jinja": true }, { - "preLaunchTask": "versiondump", + "preLaunchTask": "mariadb", "name": "Python: Flask (with reload, externally reachable)", "type": "python", "cwd": "${workspaceFolder}/backend", @@ -53,6 +55,7 @@ "FLASK_ENV": "development", "FLASK_DEBUG": "1" }, + "envFile": "${workspaceFolder}/.env.dev", "args": [ "run", "--no-debugger", @@ -61,7 +64,7 @@ "jinja": true }, { - "preLaunchTask": "versiondump", + "preLaunchTask": "mariadb", "name": "Python: Flask (externally reachable)", "type": "python", "cwd": "${workspaceFolder}/backend", @@ -72,6 +75,7 @@ "FLASK_ENV": "development", "FLASK_DEBUG": "1" }, + "envFile": "${workspaceFolder}/.env.dev", "args": [ "run", "--no-debugger", diff --git a/.vscode/settings.json b/.vscode/settings.json index ecb25f5..aa69af9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,9 +1,17 @@ { "python.pythonPath": "/usr/bin/python", - "files.exclude": { - "**/.classpath": true, - "**/.project": true, - "**/.settings": true, - "**/.factorypath": true + "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" } } \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 3c87c26..43b643d 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -17,6 +17,13 @@ "problemMatcher": [ "$tsc" ] + }, + { + "label": "mariadb", + "type": "shell", + "command": "docker-compose -f docker-compose.yml up --remove-orphans", + "isBackground": true, + "activeOnStart": false } ] } \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ef63b2d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,20 @@ +FROM tiangolo/meinheld-gunicorn-flask:python3.9 + +# Currently unusable, mariadb is not available through installer for debian 12 +# RUN apt-key adv --recv-keys --keyserver keyserver.ubuntu.com 0xcbcb082a1bb943db +# RUN curl -LsS https://downloads.mariadb.com/MariaDB/mariadb_repo_setup | bash + +RUN apt-get update +RUN apt-get upgrade -y +RUN apt-get dist-upgrade + +# In the meantime, acquire the mariadb packages through apt +RUN apt-get install -y libmariadb3 libmariadb-dev + +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 \ No newline at end of file diff --git a/backend/Pipfile b/backend/Pipfile new file mode 100644 index 0000000..d24971b --- /dev/null +++ b/backend/Pipfile @@ -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" diff --git a/backend/app.py b/backend/app.py index a36000c..7b117bf 100644 --- a/backend/app.py +++ b/backend/app.py @@ -1,4 +1,5 @@ -from flask import Flask, render_template, Response, abort, request, redirect, send_from_directory +from flask import Flask, render_template, abort, request, redirect, send_from_directory, jsonify +from flask.wrappers import Response import helpers import database import data_adapters @@ -9,7 +10,8 @@ from helpers import nocache app = Flask(__name__, static_url_path='/static') basic_auth = BasicAuth(app) -accept_entries = False +accept_entries = True + @app.route("/") def home(): @@ -35,13 +37,13 @@ def enqueue(): if not helpers.is_valid_uuid(client_id): print(request.data) abort(400) - name = request.json['name'] + name = request.json['name'].strip() song_id = request.json['id'] if request.authorization: - database.add_entry(name, song_id, client_id) - return Response('{"status":"OK"}', mimetype='text/json') + entry_id = database.add_entry(name, song_id, client_id) + return Response(f"""{{"status":"OK", "entry_id":{entry_id}}}""", mimetype='text/json') else: - if accept_entries: + if helpers.get_accept_entries(app): if not request.json: print(request.data) abort(400) @@ -51,10 +53,10 @@ def enqueue(): abort(400) name = request.json['name'] song_id = request.json['id'] - if database.check_queue_length() < app.config['MAX_QUEUE']: - if database.check_entry_quota(client_id) < app.config['ENTRY_QUOTA']: - database.add_entry(name, song_id, client_id) - return Response('{"status":"OK"}', mimetype='text/json') + if database.check_queue_length() < int(app.config['MAX_QUEUE']): + if database.check_entry_quota(client_id) < int(app.config['ENTRY_QUOTA']): + entry_id = database.add_entry(name, song_id, client_id) + return Response(f"""{{"status":"OK", "entry_id":{entry_id}}}""", mimetype='text/json') 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) else: @@ -72,7 +74,7 @@ def songlist(): @nocache @basic_auth.required def settings(): - return render_template('settings.html', app=app, auth=basic_auth.authenticate()) + return render_template('settings.html', app=app, auth=basic_auth.authenticate(), themes=helpers.get_themes()) @app.route("/settings", methods=['POST']) @@ -81,16 +83,33 @@ def settings(): def settings_post(): entryquota = request.form.get("entryquota") maxqueue = request.form.get("maxqueue") - if entryquota.isnumeric() and int(entryquota) > 0: - app.config['ENTRY_QUOTA'] = int(entryquota) + 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 + app.config['ENTRY_QUOTA'] = int(entryquota) # type: ignore else: abort(400) - if maxqueue.isnumeric and int(maxqueue) > 0: - app.config['MAX_QUEUE'] = int(maxqueue) + if maxqueue.isnumeric and int(maxqueue) > 0: # type: ignore + app.config['MAX_QUEUE'] = int(maxqueue) # type: ignore else: abort(400) - - return render_template('settings.html', app=app, auth=basic_auth.authenticate()) + if theme is not None and theme in helpers.get_themes(): + helpers.set_theme(app, theme) + else: + 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) + if changed_credentials: + return redirect("/") + else: + return render_template('settings.html', app=app, auth=basic_auth.authenticate(), themes=helpers.get_themes()) @app.route("/api/queue") @@ -119,29 +138,48 @@ def songs(): @basic_auth.required def update_songs(): database.delete_all_entries() + helpers.reset_current_event_id(app) status = database.import_songs( helpers.get_songs(helpers.get_catalog_url())) print(status) return Response('{"status": "%s" }' % status, mimetype='text/json') -@app.route("/api/songs/compl") +@app.route("/api/songs/compl") # type: ignore @nocache def get_song_completions(input_string=""): input_string = request.args.get('search', input_string) if input_string != "": - print(input_string) - list = database.get_song_completions(input_string=input_string) - return Response(json.dumps(list, ensure_ascii=False).encode('utf-8'), mimetype='text/json') + result = [list(x) for x in database.get_song_completions(input_string=input_string)] + return jsonify(result) else: return 400 -@app.route("/api/entries/delete/") +@app.route("/api/entries/delete/", methods=['GET']) @nocache @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/", 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): return Response('{"status": "OK"}', mimetype='text/json') else: @@ -158,7 +196,7 @@ def delete_entries(): return updates = database.delete_entries(request.json) if updates >= 0: - return Response('{"status": "OK", "updates": '+str(updates)+'}', mimetype='text/json') + return Response('{"status": "OK", "updates": ' + str(updates) + '}', mimetype='text/json') else: return Response('{"status": "FAIL"}', mimetype='text/json', status=400) @@ -172,6 +210,7 @@ def mark_sung(entry_id): else: return Response('{"status": "FAIL"}', mimetype='text/json') + @app.route("/api/entries/mark_transferred/") @nocache @basic_auth.required @@ -186,9 +225,8 @@ def mark_transferred(entry_id): @nocache @basic_auth.required def set_accept_entries(value): - global accept_entries if (value == '0' or value == '1'): - accept_entries = bool(int(value)) + helpers.set_accept_entries(app, bool(int(value))) return Response('{"status": "OK"}', mimetype='text/json') else: return Response('{"status": "FAIL"}', mimetype='text/json', status=400) @@ -197,8 +235,8 @@ def set_accept_entries(value): @app.route("/api/entries/accept") @nocache def get_accept_entries(): - global accept_entries - return Response('{"status": "OK", "value": '+str(int(accept_entries))+'}', mimetype='text/json') + accept_entries = helpers.get_accept_entries(app) + return Response('{"status": "OK", "value": ' + str(int(accept_entries)) + '}', mimetype='text/json') @app.route("/api/played/clear") @@ -216,6 +254,7 @@ def clear_played_songs(): @basic_auth.required def delete_all_entries(): if database.delete_all_entries(): + helpers.reset_current_event_id(app) return Response('{"status": "OK"}', mimetype='text/json') else: return Response('{"status": "FAIL"}', mimetype='text/json') @@ -227,29 +266,36 @@ def admin(): 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 def activate_job(): + helpers.load_dbconfig(app) helpers.load_version(app) - helpers.create_data_directory() database.create_entry_table() database.create_song_table() database.create_done_song_table() database.create_list_view() database.create_done_song_view() + database.create_config_table() helpers.setup_config(app) - @app.after_request def add_header(response): """ Add headers to both force latest IE rendering engine or Chrome Frame, and also to cache the rendered page for 10 minutes. """ - if not 'Cache-Control' in response.headers: - response.headers['Cache-Control'] = 'private, max-age=600' + if 'Cache-Control' not in response.headers: + response.headers['Cache-Control'] = 'private, max-age=600, no-cache, must-revalidate' return response + @app.context_processor def inject_version(): return dict(karaoqueue_version=app.config['VERSION']) diff --git a/backend/data_adapters.py b/backend/data_adapters.py index e3437bc..0136322 100644 --- a/backend/data_adapters.py +++ b/backend/data_adapters.py @@ -1,8 +1,5 @@ -def dict_from_row(row): - return dict(zip(row.keys(), row)) - def dict_from_rows(rows): - outlist=[] + outlist = [] for row in rows: - outlist.append(dict_from_row(row)) - return outlist \ No newline at end of file + outlist.append(dict(row._mapping)) + return outlist diff --git a/backend/database.py b/backend/database.py index 8fe2cd2..2c25b2c 100644 --- a/backend/database.py +++ b/backend/database.py @@ -1,190 +1,219 @@ # -*- coding: utf_8 -*- -import sqlite3 +from sqlalchemy import create_engine, engine, text import pandas from io import StringIO +from flask import current_app +import uuid song_table = "songs" entry_table = "entries" index_label = "Id" done_table = "done_songs" +sql_engine = None -def open_db(): - conn = sqlite3.connect("/tmp/karaoqueue.db") - conn.execute('PRAGMA encoding = "UTF-8";') - return conn + +def get_db_engine() -> engine.base.Engine: + global sql_engine + if (not sql_engine): + sql_engine = create_engine( + current_app.config.get("DBCONNSTRING")) # type: ignore + return sql_engine def import_songs(song_csv): print("Start importing Songs...") df = pandas.read_csv(StringIO(song_csv), sep=';') - conn = open_db() - cur = conn.cursor() - df.to_sql(song_table, conn, if_exists='replace', - index=False) - cur.execute("SELECT Count(Id) FROM songs") - num_songs = cur.fetchone()[0] - conn.close() + with get_db_engine().connect() as conn: + df.to_sql(song_table, conn, if_exists='replace', + index=False) + cur = conn.execute(text("SELECT Count(Id) FROM songs")) + num_songs = cur.fetchone()[0] # type: ignore + conn.commit() 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(): - conn = open_db() - conn.execute('CREATE TABLE IF NOT EXISTS '+entry_table + - ' (ID INTEGER PRIMARY KEY NOT NULL, Song_Id INTEGER NOT NULL, Name VARCHAR(255), Client_Id VARCHAR(36), Transferred INTEGER DEFAULT 0)') - conn.close() + with get_db_engine().connect() as conn: + stmt = text( + 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(): - conn = open_db() - conn.execute('CREATE TABLE IF NOT EXISTS '+done_table + - ' (Song_Id INTEGER PRIMARY KEY NOT NULL, Plays INTEGER)') - conn.close() + with get_db_engine().connect() as conn: + stmt = text( + 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(): - conn = open_db() - conn.execute("CREATE TABLE IF NOT EXISTS \""+song_table+"""\" ( - "Id" INTEGER, - "Title" TEXT, - "Artist" TEXT, - "Year" INTEGER, - "Duo" INTEGER, - "Explicit" INTEGER, - "Date Added" TEXT, - "Styles" TEXT, - "Languages" TEXT - )""") - conn.close() + with get_db_engine().connect() as conn: + stmt = text(f"""CREATE TABLE IF NOT EXISTS `{song_table}` ( + `Id` INTEGER, + `Title` TEXT, + `Artist` TEXT, + `Year` VARCHAR(4), + `Duo` BOOLEAN, + `Explicit` INTEGER, + `Date Added` TIMESTAMP, + `Styles` TEXT, + `Languages` TEXT + )""") + conn.execute(stmt) + conn.commit() def create_list_view(): - conn = open_db() - conn.execute("""CREATE VIEW IF NOT EXISTS [Liste] AS - SELECT Name, Title, Artist, entries.Id, songs.Id, entries.Transferred + with get_db_engine().connect() as conn: + stmt = text("""CREATE OR REPLACE VIEW `Liste` AS + SELECT Name, Title, Artist, entries.Id AS entry_ID, songs.Id AS song_ID, entries.Transferred FROM entries, songs - WHERE entries.Song_Id=songs.Id""") - conn.close() + WHERE entries.Song_Id=songs.Id + ORDER BY entries.Id ASC + """) + conn.execute(stmt) + conn.commit() def create_done_song_view(): - conn = open_db() - conn.execute("""CREATE VIEW IF NOT EXISTS [Abspielliste] AS - SELECT Artist || \" - \" || Title AS Song, Plays AS Wiedergaben + with get_db_engine().connect() as conn: + stmt = text("""CREATE OR REPLACE VIEW `Abspielliste` AS + SELECT CONCAT(Artist," - ", Title) AS Song, Plays AS Wiedergaben FROM songs, done_songs WHERE done_songs.Song_Id=songs.Id""") - conn.close() + conn.execute(stmt) + conn.commit() + + +def create_config_table(): + with get_db_engine().connect() as conn: + stmt = text("""CREATE TABLE IF NOT EXISTS `config` ( + `Key` VARCHAR(50) NOT NULL PRIMARY KEY, + `Value` TEXT + )""") + conn.execute(stmt) + conn.commit() def get_list(): - conn = open_db() - conn.row_factory = sqlite3.Row - cur = conn.cursor() - cur.execute("SELECT * FROM Liste") + with get_db_engine().connect() as conn: + stmt = text("SELECT * FROM Liste") + cur = conn.execute(stmt) return cur.fetchall() def get_played_list(): - conn = open_db() - cur = conn.cursor() - cur.execute("SELECT * FROM Abspielliste") + with get_db_engine().connect() as conn: + stmt = text("SELECT * FROM Abspielliste") + cur = conn.execute(stmt) return cur.fetchall() def get_song_list(): - conn = open_db() - cur = conn.cursor() - cur.execute("SELECT Artist || \" - \" || Title AS Song, Id FROM songs;") + with get_db_engine().connect() as conn: + stmt = text("SELECT Artist || \" - \" || Title AS Song, Id FROM songs;") + cur = conn.execute(stmt) return cur.fetchall() def get_song_completions(input_string): - conn = open_db() - cur = conn.cursor() - # Don't look, it burns... - prepared_string = "%{0}%".format( - input_string).upper() # "Test" -> "%TEST%" - print(prepared_string) - cur.execute( - "SELECT Title || \" - \" || Artist AS Song, Id FROM songs WHERE REPLACE(REPLACE(REPLACE(REPLACE(UPPER( SONG ),'ö','Ö'),'ü','Ü'),'ä','Ä'),'ß','ẞ') LIKE (?) LIMIT 20;", (prepared_string,)) - return cur.fetchall() + with get_db_engine().connect() as conn: + prepared_string = f"%{input_string.upper()}%" + stmt = text( + "SELECT CONCAT(Artist, ' - ', Title) AS Song, Id FROM songs WHERE CONCAT(Artist, ' - ', Title) LIKE :prepared_string LIMIT 20;") + cur = conn.execute( + stmt, {"prepared_string": prepared_string}) # type: ignore + return cur.fetchall() def add_entry(name, song_id, client_id): - conn = open_db() - cur = conn.cursor() - cur.execute( - "INSERT INTO entries (Song_Id,Name,Client_Id) VALUES(?,?,?);", (song_id, name, client_id)) - conn.commit() - conn.close() - return + with get_db_engine().connect() as conn: + stmt = text( + "INSERT INTO entries (Song_Id,Name,Client_Id) VALUES (:par_song_id,:par_name,:par_client_id) RETURNING entries.ID;") + 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): - conn = open_db() - cur = conn.cursor() - cur.execute("""SELECT Song_Id FROM entries WHERE Id=?""", (entry_id,)) - song_id = cur.fetchone()[0] - cur.execute("""INSERT OR REPLACE INTO done_songs (Song_Id, Plays) - VALUES("""+str(song_id)+""", - COALESCE( - (SELECT Plays FROM done_songs - WHERE Song_Id="""+str(song_id)+"), 0) + 1)" - ) - conn.commit() - delete_entry(entry_id) - conn.close() + with get_db_engine().connect() as conn: + stmt = text("SELECT Song_Id FROM entries WHERE Id=:par_entry_id") + cur = conn.execute(stmt, {"par_entry_id": entry_id}) # type: ignore + song_id = cur.fetchone()[0] # type: ignore + 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) return True def toggle_transferred(entry_id): - conn = open_db() - cur = conn.cursor() - cur.execute("SELECT Transferred FROM entries WHERE ID =?", (entry_id,)) - marked = cur.fetchall()[0][0] - if(marked == 0): - cur.execute( - "UPDATE entries SET Transferred = 1 WHERE ID =?", (entry_id,)) - else: - cur.execute( - "UPDATE entries SET Transferred = 0 WHERE ID =?", (entry_id,)) - conn.commit() - conn.close() + with get_db_engine().connect() as conn: + cur = conn.execute(text("SELECT Transferred FROM entries WHERE ID = :par_entry_id"), + {"par_entry_id": entry_id}) # type: ignore + marked = cur.fetchall()[0][0] + if (marked == 0): + conn.execute(text("UPDATE entries SET Transferred = 1 WHERE ID = :par_entry_id"), + {"par_entry_id": entry_id}) # type: ignore + else: + conn.execute(text("UPDATE entries SET Transferred = 0 WHERE ID = :par_entry_id"), + {"par_entry_id": entry_id}) # type: ignore + conn.commit() return True def check_entry_quota(client_id): - conn = open_db() - cur = conn.cursor() - cur.execute( - "SELECT Count(*) FROM entries WHERE entries.Client_Id = ?", (client_id,)) + with get_db_engine().connect() as conn: + cur = conn.execute(text("SELECT Count(*) FROM entries WHERE entries.Client_Id = :par_client_id"), + {"par_client_id": client_id}) # type: ignore return cur.fetchall()[0][0] def check_queue_length(): - conn = open_db() - cur = conn.cursor() - cur.execute("SELECT Count(*) FROM entries") + with get_db_engine().connect() as conn: + cur = conn.execute(text("SELECT Count(*) FROM entries")) return cur.fetchall()[0][0] def clear_played_songs(): - conn = open_db() - cur = conn.cursor() - cur.execute("DELETE FROM done_songs") - conn.commit() - conn.close() + with get_db_engine().connect() as conn: + conn.execute(text("DELETE FROM done_songs")) + conn.commit() 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): - conn = open_db() - cur = conn.cursor() - cur.execute("DELETE FROM entries WHERE id=?", (id,)) - conn.commit() - conn.close() + with get_db_engine().connect() as conn: + conn.execute(text("DELETE FROM entries WHERE id= :par_id"), { + "par_id": id}) # type: ignore + conn.commit() return True @@ -193,20 +222,76 @@ def delete_entries(ids): 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() + with get_db_engine().connect() as conn: + cur = conn.execute(text("DELETE FROM entries WHERE id= :par_id"), { + "par_id": idlist}) + conn.commit() return cur.rowcount - except sqlite3.Error as error: + except Exception: return -1 -def delete_all_entries(): - conn = open_db() - cur = conn.cursor() - cur.execute("DELETE FROM entries") - conn.commit() - conn.close() +def delete_all_entries() -> bool: + with get_db_engine().connect() as conn: + conn.execute(text("DELETE FROM entries")) + conn.commit() return True + + +def get_config(key: str) -> str: + try: + with get_db_engine().connect() as conn: + 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: + print(f"Setting config {key} to {value}") + with get_db_engine().connect() as conn: + 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 + + +def get_config_list() -> dict: + with get_db_engine().connect() as conn: + cur = conn.execute(text("SELECT * FROM config")) + result_dict = {} + for row in cur.fetchall(): + result_dict[row[0]] = row[1] + 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") diff --git a/backend/docker/Dockerfile b/backend/docker/Dockerfile deleted file mode 100644 index ac45abf..0000000 --- a/backend/docker/Dockerfile +++ /dev/null @@ -1,11 +0,0 @@ -FROM tiangolo/uwsgi-nginx-flask:python3.7 - -RUN pip install requests - -RUN pip install pandas - -RUN pip install Flask-BasicAuth - -RUN pip install bs4 - -COPY ../app /app \ No newline at end of file diff --git a/backend/helpers.py b/backend/helpers.py index 17545a6..9e3cde4 100644 --- a/backend/helpers.py +++ b/backend/helpers.py @@ -1,30 +1,26 @@ import requests from bs4 import BeautifulSoup -import json import os import uuid -from flask import make_response +from flask import make_response, Flask from functools import wraps, update_wrapper from datetime import datetime - -data_directory = "data" -config_file = data_directory+"/config.json" - -def create_data_directory(): - if not os.path.exists(data_directory): - os.makedirs(data_directory) +import database def get_catalog_url(): r = requests.get('https://www.karafun.de/karaoke-song-list.html') 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 + def get_songs(url): r = requests.get(url) return r.text + def is_valid_uuid(val): try: uuid.UUID(str(val)) @@ -32,12 +28,14 @@ def is_valid_uuid(val): except ValueError: return False -def check_config_exists(): - return os.path.isfile(config_file) -def load_version(app): +def check_config_exists(): + return database.check_config_table() + + +def load_version(app: Flask): if os.environ.get("SOURCE_VERSION"): - app.config['VERSION'] = os.environ.get("SOURCE_VERSION")[0:7] + app.config['VERSION'] = os.environ.get("SOURCE_VERSION")[0:7] # type: ignore # noqa: E501 elif os.path.isfile(".version"): with open('.version', 'r') as file: data = file.read().replace('\n', '') @@ -48,32 +46,132 @@ def load_version(app): else: app.config['VERSION'] = "" -def setup_config(app): - if check_config_exists(): - config = json.load(open(config_file)) - with open(config_file, 'r') as handle: - config = json.load(handle) - print("Loaded existing config") + +def load_dbconfig(app: Flask): + if os.environ.get("FLASK_ENV") == "development": + app.config['DBCONNSTRING'] = os.environ.get("DBSTRING") else: - config = {'username': 'admin', 'password': 'changeme', 'entryquota': 3, 'maxqueue': 20} - with open(config_file, 'w') as handle: - json.dump(config, handle, indent=4, sort_keys=True) - print("Wrote new config") + if os.environ.get("DEPLOYMENT_PLATFORM") == "Heroku": + if os.environ.get("JAWSDB_MARIA_URL"): + app.config['DBCONNSTRING'] = os.environ.get("JAWSDB_MARIA_URL") + else: + app.config['DBCONNSTRING'] = "" + if os.environ.get("DEPLOYMENT_PLATFORM") == "Docker": + if os.environ.get("DBSTRING"): + app.config['DBCONNSTRING'] = os.environ.get("DBSTRING") + else: + app.config['DBCONNSTRING'] = "" + elif os.path.isfile(".dbconn"): + with open('.dbconn', 'r') as file: + data = file.read().replace('\n', '') + if data: + app.config['DBCONNSTRING'] = data + else: + app.config['DBCONNSTRING'] = "" + 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.""") + +# Check if config exists in DB, if not, create it. + + +def setup_config(app: Flask): + if check_config_exists() is False: + print("No config found, creating new config") + initial_username = os.environ.get("INITIAL_USERNAME") + initial_password = os.environ.get("INITIAL_PASSWORD") + if initial_username is None: + 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': 2, + 'maxqueue': 10, + 'entries_allowed': 1, + 'theme': 'default.css'} + for key, value in default_config.items(): + database.set_config(key, value) + print("Created new config") + database.init_event_id() + config = database.get_config_list() app.config['BASIC_AUTH_USERNAME'] = config['username'] app.config['BASIC_AUTH_PASSWORD'] = config['password'] app.config['ENTRY_QUOTA'] = config['entryquota'] app.config['MAX_QUEUE'] = config['maxqueue'] + app.config['ENTRIES_ALLOWED'] = bool(config['entries_allowed']) + app.config['THEME'] = config['theme'] + app.config['EVENT_ID'] = database.get_event_id() +# set queue admittance + + +def set_accept_entries(app: Flask, allowed: bool): + if allowed: + app.config['ENTRIES_ALLOWED'] = True + database.set_config('entries_allowed', '1') + else: + app.config['ENTRIES_ALLOWED'] = False + database.set_config('entries_allowed', '0') + +# get queue admittance + + +def get_accept_entries(app: Flask) -> bool: + state = bool(int(database.get_config('entries_allowed'))) + app.config['ENTRIES_ALLOWED'] = state + return state + + +# Write settings from current app.config to DB +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']} + for key, value in config.items(): + database.set_config(key, value) + +# Get available themes from themes directory + + +def get_themes(): + themes = [] + for theme in os.listdir('./static/css/themes'): + themes.append(theme) + return themes + +# Set theme + + +def set_theme(app: Flask, theme: str): + if theme in get_themes(): + app.config['THEME'] = theme + database.set_config('theme', theme) + else: + 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): @wraps(view) def no_cache(*args, **kwargs): response = make_response(view(*args, **kwargs)) - response.headers['Last-Modified'] = datetime.now() + response.headers['Last-Modified'] = datetime.now() # type: ignore response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0' response.headers['Pragma'] = 'no-cache' response.headers['Expires'] = '-1' return response - - return update_wrapper(no_cache, view) \ No newline at end of file + + return update_wrapper(no_cache, view) diff --git a/backend/wsgi.py b/backend/main.py similarity index 73% rename from backend/wsgi.py rename to backend/main.py index 210a38e..6026b0f 100644 --- a/backend/wsgi.py +++ b/backend/main.py @@ -1,4 +1,4 @@ from app import app if __name__ == "__main__": - app.run() \ No newline at end of file + app.run() diff --git a/backend/requirements.txt b/backend/requirements.txt index 04324d8..01dcd23 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,5 +1,36 @@ -requests -pandas -Flask-BasicAuth -bs4 -gunicorn \ No newline at end of file +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.3.2 +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.31.0 +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 diff --git a/backend/static/css/style.css b/backend/static/css/style.css index b7088b6..52298d9 100644 --- a/backend/static/css/style.css +++ b/backend/static/css/style.css @@ -1,77 +1,201 @@ -body { - padding-top: 5rem; +: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); + + /* 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 { - height: auto; - min-height: 100%; + height: auto; + min-height: 100%; } main { - padding-bottom: 60px; /* Höhe des Footers */ + padding-bottom: 60px; + /* Höhe des Footers */ } .footer { - margin-top: -60px; - width: 100%; - height: 60px; - /* Set the fixed height of the footer here */ - /*line-height: 60px; /* Vertically center the text there */ - background-color: #f5f5f5; + margin-top: -60px; + width: 100%; + height: 60px; + /* Set the fixed height of the footer here */ + /*line-height: 60px; /* Vertically center the text there */ + background-color: var(--background-color-var); } .topbutton { - width: 100%; + width: 100%; } table td { - overflow: hidden; - text-overflow: ellipsis; + overflow: hidden; + text-overflow: ellipsis; } table.entries tbody tr[data-index="0"] { - background-color: #007bff80; - font-weight: 600; + background-color: #007bff80; + font-weight: 600; } table.entries tbody tr[data-index="1"] { - background-color: #007bff40; - font-weight: 500; + background-color: #007bff40; + font-weight: 500; } table.entries tbody tr[data-index="2"] { - background-color: #007bff20; - font-weight: 400; + background-color: #007bff20; + font-weight: 400; } table.entries tbody tr[data-index="3"] { - background-color: #007bff10; + background-color: #007bff10; } table td:first-child { - max-width: 200px !important; + max-width: 200px !important; +} + +.fa-solid { + vertical-align: auto; } @media (min-width: 768px) { - .topbutton { - width: auto; - } + .topbutton { + width: auto; + } } @media print { - body { - font-size: 1.3em; - } + body { + font-size: 1.3em; + } - .footer { - display: none !important; - } + .footer { + display: none !important; + } - .admincontrols { - display: none; - } + .admincontrols { + 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) { + :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); + + /* Common */ + --background-color: #121212; + --background-color-var: #232323; + --text-color: #f5f5f5; + --text-color-var: #a2a2a2; + + /* Modals */ + --modal-background-color: #121212; + --modal-separator-color: #232323; + --modal-close-color: #f5f5f5; + + /* Tables */ + --table-border-color: #232323; + + /* Input */ + --input-background-color: #343434; + } } \ No newline at end of file diff --git a/backend/static/css/themes/default.css b/backend/static/css/themes/default.css new file mode 100644 index 0000000..e69de29 diff --git a/backend/static/css/themes/stuk.css b/backend/static/css/themes/stuk.css new file mode 100644 index 0000000..5bc0c8a --- /dev/null +++ b/backend/static/css/themes/stuk.css @@ -0,0 +1,91 @@ +.navbar { + background: #090a28 !important; +} + +.navbar .navbar-toggle:hover, +.navbar .navbar-toggle:focus { + background-color: #900000 !important; +} + +.navbar .navbar-toggle { + border: none; +} + +.navbar .navbar-nav>.open>a, +.navbar .navbar-nav>.open>a:hover, +.navbar .navbar-nav>.open>a:focus { + color: #CF2323 !important; + background-color: #050515 !important; +} + +.btn-primary { + background-color: #15175b; + border-color: #15175b; +} + +.btn-primary:hover, +.btn-primary:focus { + background-color: #0e103e; + background-position: 0 -15px; +} + +.btn-primary:active, +.btn-primary.active { + background-color: #0e103e; + border-color: #0e103e; +} + +.btn-primary.disabled, +.btn-primary:disabled, +.btn-primary[disabled] { + background-color: #0e103e; + background-image: none; +} + +.dropdown-menu>li>a:hover, +.dropdown-menu>li>a:focus { + color: #8A0711; +} + +.dropdown-menu>.active>a, +.dropdown-menu>.active>a:hover, +.dropdown-menu>.active>a:focus { + background-color: #2e6da4; + background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%); + background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4)); + background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0); + background-repeat: repeat-x; +} + +.navbar .navbar-nav>li>a:hover { + color: #b60000; +} + +.form-control:focus { + border-color: rgba(21, 23, 91, 0.8); + outline: 0; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), + 0 0 8px rgba(21, 23, 91, 0.6); + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075) inset, + 0 0 8px rgba(21, 23, 91, 0.6); +} + +a { + color: #900000; +} + +a:hover, +a:focus { + color: #670000; +} + +.navbar-brand { + display: flex; + align-items: center; +} + +.navbar-brand > img { + height: 4rem; +} \ No newline at end of file diff --git a/backend/templates/base.html b/backend/templates/base.html index f243850..58c2bda 100644 --- a/backend/templates/base.html +++ b/backend/templates/base.html @@ -6,28 +6,36 @@ + {% block title %}{% endblock %} - KaraoQueue - + + + + - + - + + + + @@ -73,9 +81,9 @@ {% if not auth %} Login {% endif %} - - KaraoQueue {{karaoqueue_version}} - © 2019-21 - Phillip + Github + {{karaoqueue_version}} - 2019-23 - Phillip Kühne @@ -83,27 +91,26 @@ - + - - + + - + + src="https://unpkg.com/bootstrap-table@1.21.2/dist/extensions/auto-refresh/bootstrap-table-auto-refresh.min.js"> + + {% block extrajs %}{% endblock %} diff --git a/backend/templates/main.html b/backend/templates/main.html index 28072e4..0cbef50 100644 --- a/backend/templates/main.html +++ b/backend/templates/main.html @@ -17,6 +17,7 @@ Name Song Künstler + @@ -29,7 +30,48 @@ $.getJSON("/api/entries/accept", (data) => { $("#bfb").addClass("disabled") $("#bfb").prop("aria-disabled",true); $("#bfb").prop("tabindex","-1"); + $("#bfb").wrap(""); + $(".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 "" + } + 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() + } + }) + } + }) +} + {% endblock %} \ No newline at end of file diff --git a/backend/templates/main_admin.html b/backend/templates/main_admin.html index 5234a88..4649063 100644 --- a/backend/templates/main_admin.html +++ b/backend/templates/main_admin.html @@ -1,15 +1,13 @@ - - {% extends 'base.html' %} {% block title %}Warteliste-Admin{% endblock %} {% block content %}
@@ -18,30 +16,19 @@ table td:nth-child(2) { - +
- +
- - - + + + @@ -52,16 +39,19 @@ table td:nth-child(2) { {% block extrajs %} {% endblock %} \ No newline at end of file diff --git a/backend/templates/settings.html b/backend/templates/settings.html index 6678bbf..7722183 100644 --- a/backend/templates/settings.html +++ b/backend/templates/settings.html @@ -10,9 +10,32 @@

+

+ + +

+ +

+ + +

+

+ + +

- +
+ Current config: +
{% for key, val in config.items() %}{{key}}: {{val}}
{% endfor %}
+
{% endblock %} {% block extrajs %} {% endblock %} \ No newline at end of file diff --git a/backend/templates/songlist.html b/backend/templates/songlist.html index eb0221e..5799757 100644 --- a/backend/templates/songlist.html +++ b/backend/templates/songlist.html @@ -46,7 +46,7 @@ var items = []; $.each(data, function (key, val) { items.push(" -
NameSongKünstlerNameSongKünstler Aktionen
" + val[0] + `