Compare commits

..

28 Commits

Author SHA1 Message Date
5efa21924b Update dockerfile to adapt to new debian version 2023-06-30 15:59:04 +02:00
4c806c3550 update docker-compose container version 2023-06-30 15:58:34 +02:00
c21e6300e9 Merge pull request #70 from PhoenixTwoFive/64-defaults-for-entry-limitation-too-high
lowered maxqueue to 10 and participant quota to 2
2023-06-30 15:34:56 +02:00
7525708dce lowered maxqueue to 10 and participant quota to 2 2023-06-30 15:32:13 +02:00
37d95f61b2 Merge pull request #69 from PhoenixTwoFive/63-input-sanitization
63 input sanitization
2023-06-30 15:29:46 +02:00
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
20 changed files with 630 additions and 269 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

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
# 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

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,12 +1,16 @@
FROM tiangolo/meinheld-gunicorn-flask:python3.9
RUN apt-key adv --recv-keys --keyserver keyserver.ubuntu.com 0xcbcb082a1bb943db
RUN curl -LsS https://downloads.mariadb.com/MariaDB/mariadb_repo_setup | bash
# 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

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.wrappers import Request, Response
from flask.wrappers import Response
import helpers
import database
import data_adapters
@ -12,6 +12,7 @@ app = Flask(__name__, static_url_path='/static')
basic_auth = BasicAuth(app)
accept_entries = True
@app.route("/")
def home():
if basic_auth.authenticate():
@ -36,11 +37,11 @@ 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 helpers.get_accept_entries(app):
if not request.json:
@ -54,8 +55,8 @@ def enqueue():
song_id = request.json['id']
if database.check_queue_length() < int(app.config['MAX_QUEUE']):
if database.check_entry_quota(client_id) < int(app.config['ENTRY_QUOTA']):
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:
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:
@ -95,7 +96,7 @@ def settings_post():
else:
abort(400)
if theme is not None and theme in helpers.get_themes():
helpers.set_theme(app,theme)
helpers.set_theme(app, theme)
else:
abort(400)
if username != "" and username != app.config['BASIC_AUTH_USERNAME']:
@ -137,18 +138,18 @@ 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") # type: ignore
@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)
result = [list(x) for x in database.get_song_completions(input_string=input_string)]
return jsonify(result)
@ -156,10 +157,29 @@ def get_song_completions(input_string=""):
return 400
@app.route("/api/entries/delete/<entry_id>")
@app.route("/api/entries/delete/<entry_id>", 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/<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):
return Response('{"status": "OK"}', mimetype='text/json')
else:
@ -176,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)
@ -190,6 +210,7 @@ def mark_sung(entry_id):
else:
return Response('{"status": "FAIL"}', mimetype='text/json')
@app.route("/api/entries/mark_transferred/<entry_id>")
@nocache
@basic_auth.required
@ -205,7 +226,7 @@ def mark_transferred(entry_id):
@basic_auth.required
def set_accept_entries(value):
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')
else:
return Response('{"status": "FAIL"}', mimetype='text/json', status=400)
@ -215,7 +236,7 @@ def set_accept_entries(value):
@nocache
def get_accept_entries():
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")
@ -233,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')
@ -244,6 +266,12 @@ 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)
@ -257,17 +285,17 @@ def activate_job():
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:
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'])

View File

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

View File

@ -1,11 +1,10 @@
# -*- coding: utf_8 -*-
from email.mime import base
from MySQLdb import Connection
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"
@ -18,7 +17,6 @@ sql_engine = None
def get_db_engine() -> engine.base.Engine:
global sql_engine
if (not sql_engine):
print(current_app.config.get("DBCONNSTRING"))
sql_engine = create_engine(
current_app.config.get("DBCONNSTRING")) # type: ignore
return sql_engine
@ -136,11 +134,11 @@ def get_song_completions(input_string):
def add_entry(name, song_id, client_id):
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);")
conn.execute(stmt, {"par_song_id": song_id, "par_name": name,
"par_client_id": client_id}) # type: ignore
"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 True
return cur.fetchone()[0] # type: ignore
def add_sung_song(entry_id):
@ -191,6 +189,26 @@ def clear_played_songs():
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):
with get_db_engine().connect() as conn:
conn.execute(text("DELETE FROM entries WHERE id= :par_id"), {
@ -209,7 +227,7 @@ def delete_entries(ids):
"par_id": idlist})
conn.commit()
return cur.rowcount
except Exception as error:
except Exception:
return -1
@ -227,7 +245,7 @@ def get_config(key: str) -> str:
text("SELECT `Value` FROM config WHERE `Key`= :par_key"), {"par_key": key}) # type: ignore
conn.commit()
return cur.fetchall()[0][0]
except IndexError as error:
except IndexError:
return ""
@ -256,9 +274,24 @@ def check_config_table() -> bool:
if conn.dialect.has_table(conn, 'config'):
# type: ignore
# type: ignore
if (conn.execute(text("SELECT COUNT(*) FROM config")).fetchone()[0] > 0): # 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
from bs4 import BeautifulSoup
import json
import os
import uuid
from flask import make_response, Flask
@ -8,6 +7,7 @@ from functools import wraps, update_wrapper
from datetime import datetime
import database
def get_catalog_url():
r = requests.get('https://www.karafun.de/karaoke-song-list.html')
soup = BeautifulSoup(r.content, 'html.parser')
@ -69,13 +69,15 @@ def load_dbconfig(app: Flask):
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.")
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() == False:
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")
@ -89,13 +91,14 @@ def setup_config(app: Flask):
exit()
default_config = {'username': initial_username,
'password': initial_password,
'entryquota': 3,
'maxqueue': 20,
'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']
@ -103,6 +106,7 @@ def setup_config(app: Flask):
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
@ -151,6 +155,15 @@ def set_theme(app: Flask, theme: str):
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):

View File

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

View File

@ -1,30 +1,36 @@
autopep8
beautifulsoup4
bs4
certifi
charset-normalizer
click
Flask
Flask-BasicAuth
greenlet
gunicorn
idna
itsdangerous
Jinja2
mariadb
MarkupSafe
mysql
mysqlclient
numpy
pandas
pycodestyle
PyMySQL
python-dateutil
pytz
requests
six
soupsieve
SQLAlchemy
toml
urllib3
Werkzeug
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

View File

@ -1,4 +1,180 @@
: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);
}
body {
padding-top: 5rem;
background-color: var(--background-color);
}
html,
body {
height: 100%;
}
.site {
height: auto;
min-height: 100%;
}
main {
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: var(--background-color-var);
}
.topbutton {
width: 100%;
}
table td {
overflow: hidden;
text-overflow: ellipsis;
}
table.entries tbody tr[data-index="0"] {
background-color: #007bff80;
font-weight: 600;
}
table.entries tbody tr[data-index="1"] {
background-color: #007bff40;
font-weight: 500;
}
table.entries tbody tr[data-index="2"] {
background-color: #007bff20;
font-weight: 400;
}
table.entries tbody tr[data-index="3"] {
background-color: #007bff10;
}
table td:first-child {
max-width: 200px !important;
}
.fa-solid {
vertical-align: auto;
}
@media (min-width: 768px) {
.topbutton {
width: auto;
}
}
@media print {
body {
font-size: 1.3em;
}
.footer {
display: none !important;
}
.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);
@ -6,189 +182,20 @@
--navbar-text-color-active: rgba(255, 255, 255, 1);
/* Common */
--background-color: #ffffff;
--background-color-var: #f5f5f5;
--text-color: #212529;
--text-color-var: #343a40;
--background-color: #121212;
--background-color-var: #232323;
--text-color: #f5f5f5;
--text-color-var: #a2a2a2;
/* Modals */
--modal-background-color: #ffffff;
--modal-separator-color: #dee2e6;
--modal-close-color: #212529;
--modal-background-color: #121212;
--modal-separator-color: #232323;
--modal-close-color: #f5f5f5;
/* Tables */
--table-border-color: #dee2e6;
--table-border-color: #232323;
/* Input */
--input-background-color: #ffffff;
}
body {
padding-top: 5rem;
background-color: var(--background-color);
}
html,
body {
height: 100%;
}
.site {
height: auto;
min-height: 100%;
}
main {
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: var(--background-color-var);
}
.topbutton {
width: 100%;
}
table td {
overflow: hidden;
text-overflow: ellipsis;
}
table.entries tbody tr[data-index="0"] {
background-color: #007bff80;
font-weight: 600;
}
table.entries tbody tr[data-index="1"] {
background-color: #007bff40;
font-weight: 500;
}
table.entries tbody tr[data-index="2"] {
background-color: #007bff20;
font-weight: 400;
}
table.entries tbody tr[data-index="3"] {
background-color: #007bff10;
}
table td:first-child {
max-width: 200px !important;
}
.fa-solid {
vertical-align: auto;
}
@media (min-width: 768px) {
.topbutton {
width: auto;
}
}
@media print {
body {
font-size: 1.3em;
}
.footer {
display: none !important;
}
.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)
}
@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;
}
--input-background-color: #343434;
}
}

View File

@ -15,9 +15,12 @@
<!-- Bootstrap-Tables -->
<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 -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css"
integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.2.1/dist/css/bootstrap.min.css" integrity="sha384-GJzZqFGwb1QTTN6wy59ffF1BuGJpLSa9DkKMp0DgiMDm4iYMj70gZWKYbI706tWS" crossorigin="anonymous">
<!-- Custom styles for this template -->
<link href="static/css/style.css" rel="stylesheet">
@ -88,16 +91,12 @@
<!-- Bootstrap core JavaScript
================================================== -->
<!-- Placed at the end of the document so the pages load faster -->
<script src="https://code.jquery.com/jquery-3.2.1.min.js"
integrity="sha384-xBuQ/xzmlsLoJpyjoggmTEz8OWUFM0/RC5BsqQBDX2v5cMvDHcMakNTNrHIW2I5f" crossorigin="anonymous">
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.4/jquery.min.js"
integrity="sha512-pumBsjNRGGqkPzKHndZMaAG+bir374sORyzM3uulLV14lN5LyykqNk8eEeUlUkB3U0M4FApyaHraT65ihJhDpQ=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js"
integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous">
</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://cdn.jsdelivr.net/npm/popper.js@1.14.6/dist/umd/popper.min.js" integrity="sha384-wHAiFfRlMFy6i5SRaxvfOCifBUQy1xHdJ/yoi7FRNXMRBu5WHdZYu1hA6ZOblgut" crossorigin="anonymous"></script>
<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 src="https://cdnjs.cloudflare.com/ajax/libs/bootbox.js/4.4.0/bootbox.min.js"
integrity="sha256-4F7e4JsAJyLUdpP7Q8Sah866jCOhv72zU5E8lIRER4w=" crossorigin="anonymous">
</script>
@ -105,10 +104,13 @@
<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://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 %}
<script>
$(document).ready(function () {
loadOrGenerateClientId()
checkEventID()
// get current URL path and assign 'active' class
var pathname = window.location.pathname;
$('.navbar-nav > li > a[href="' + pathname + '"]').parent().addClass('active');
@ -130,6 +132,50 @@
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>
</body>

View File

@ -17,6 +17,7 @@
<th data-field="Name">Name</th>
<th data-field="Title">Song</th>
<th data-field="Artist">Künstler</th>
<th scope="col" data-formatter="TableActionsFormatter"></th>
</tr>
</thead>
</table>
@ -34,5 +35,43 @@ $.getJSON("/api/entries/accept", (data) => {
$('[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>
{% endblock %}

View File

@ -26,9 +26,9 @@
<thead>
<tr>
<th data-field="state" data-checkbox="true"></th>
<th scope="col" data-field="Name">Name</th>
<th scope="col" data-field="Title">Song</th>
<th scope="col" data-field="Artist">Künstler</th>
<th scope="col" data-field="Name" data-formatter="CopyFormatter">Name</th>
<th scope="col" data-field="Title"data-formatter="CopyFormatter">Song</th>
<th scope="col" data-field="Artist"data-formatter="CopyFormatter">Künstler</th>
<th scope="col" data-formatter="TableActions">Aktionen</th>
</tr>
</thead>
@ -185,21 +185,36 @@
});
}
function TableActions(value, row, index) {
console.log("Value: " + value + ", Row: " + row + ", Index: " + index)
console.log(row)
let outerHTML = ""
if (row.Transferred == 1) {
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('" + 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 {
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('" + 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;
}
function CopyFormatter(value, row, index) {
let escapedString = value.replace("\"","\\\"").replace("\'", "\\\'")
return "<span onclick='copyAndNotify(this.innerText)'>"+value+"</span>";
}
function getIdSelections() {
return $.map($("#entrytable").bootstrapTable('getSelections'), function (row) {
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>
{% endblock %}

View File

@ -67,6 +67,9 @@
e.preventDefault();
submitModal();
});
$('#enqueueModal').on('shown.bs.modal', function (e) {
$("#singerNameInput").focus();
})
});
@ -79,7 +82,7 @@
$.ajax({
type: 'POST',
url: '/api/enqueue',
data: JSON.stringify(data), // or JSON.stringify ({name: 'jonas'}),
data: JSON.stringify(data),
success: success_callback,
statusCode: {
423: blocked_callback
@ -96,7 +99,18 @@
function submitModal() {
var name = $("#singerNameInput").val();
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');
window.location.href = '/#end';
}, function (response) {

View File

@ -6,7 +6,7 @@ secrets:
services:
karaoqueue:
image: "ghcr.io/phoenixtwofive/karaoqueue:v2023.03.2"
image: "ghcr.io/phoenixtwofive/karaoqueue:v2023.06"
build: .
restart: always
ports: