Compare commits

..

2 Commits

Author SHA1 Message Date
8c19de74c2 Disable registration when Event is ended. 2020-10-03 23:51:27 +02:00
9b31131d5b Add appState
appState contains variables that indicate whether an event is
currently ongoing, and whether registration is currently allowed.
2020-10-03 23:41:45 +02:00
73 changed files with 14500 additions and 10939 deletions

7
.gitignore vendored
View File

@ -131,10 +131,3 @@ dmypy.json
# Test data
data/
# Node Modules
node_modules/
# Version identification file
.version

54
.vscode/launch.json vendored
View File

@ -4,71 +4,35 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{"name":"Python: Flask","type":"python","request":"launch","module":"flask","env":{"FLASK_APP":"backend/app/main.py","FLASK_ENV":"development","FLASK_DEBUG":"1"},"args":["run","--no-debugger","--no-reload"],"jinja":true},
{"name":"Python: Flask (with reload)","type":"python","request":"launch","module":"flask","env":{"FLASK_APP":"backend/app/main.py","FLASK_ENV":"development","FLASK_DEBUG":"1"},"args":["run","--no-debugger"],"jinja":true},
{
"preLaunchTask": "versiondump",
"name": "Python: Flask",
"type": "python",
"cwd": "${workspaceFolder}/backend",
"request": "launch",
"module": "flask",
"env": {
"FLASK_APP": "backend/app.py",
"FLASK_ENV": "development",
"FLASK_DEBUG": "1"
},
"args": [
"run",
"--no-debugger",
"--no-reload"
],
"jinja": true
},
{
"preLaunchTask": "versiondump",
"name": "Python: Flask (with reload)",
"type": "python",
"cwd": "${workspaceFolder}/backend",
"request": "launch",
"module": "flask",
"env": {
"FLASK_APP": "backend/app.py",
"FLASK_ENV": "development",
"FLASK_DEBUG": "1"
},
"args": [
"run",
"--no-debugger"
],
"jinja": true
},
{
"preLaunchTask": "versiondump",
"name": "Python: Flask (with reload, externally reachable)",
"type": "python",
"cwd": "${workspaceFolder}/backend",
"request": "launch",
"module": "flask",
"env": {
"FLASK_APP": "backend/app.py",
"FLASK_APP": "backend/app/main.py",
"FLASK_ENV": "development",
"FLASK_DEBUG": "1"
},
"args": [
"run",
"--no-debugger",
"--host=0.0.0.0"
"--host='0.0.0.0'"
],
"jinja": true
},
{
"preLaunchTask": "versiondump",
"name": "Python: Flask (externally reachable)",
"type": "python",
"cwd": "${workspaceFolder}/backend",
"request": "launch",
"module": "flask",
"env": {
"FLASK_APP": "backend/app.py",
"FLASK_APP": "backend/app/main.py",
"FLASK_ENV": "development",
"FLASK_DEBUG": "1"
},
@ -76,7 +40,7 @@
"run",
"--no-debugger",
"--no-reload",
"--host=0.0.0.0"
"--host='0.0.0.0'"
],
"jinja": true
},

7
.vscode/tasks.json vendored
View File

@ -4,13 +4,6 @@
"version": "2.0.0",
"tasks": [
{
"label": "versiondump",
"type": "shell",
"command": "echo \"$(git rev-parse --abbrev-ref HEAD)-$(git describe)\"> ${workspaceFolder}/backend/.version",
"problemMatcher": []
},
{
"label": "Start Frontend",
"type": "npm",
"script": "start",
"path": "frontend/ng-karaoqueue/",

View File

@ -1,19 +0,0 @@
# This file specifies files that are *not* uploaded to Google Cloud Platform
# using gcloud. It follows the same syntax as .gitignore, with the addition of
# "#!include" directives (which insert the entries of the given .gitignore-style
# file at that point).
#
# For more information, run:
# $ gcloud topic gcloudignore
#
.gcloudignore
# If you would like to upload your .git directory, .gitignore file or files
# from your .gitignore file, remove the corresponding line
# below:
.git
.gitignore
# Python pycache:
__pycache__/
# Ignored by the build system
/setup.cfg

24
backend/.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,24 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch via NPM",
"cwd": "${workspaceFolder}/karaoqueue-backend",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run-script",
"debug"
],
"port": 9229,
"skipFiles": [
"<node_internals>/**"
],
"preLaunchTask": "npm: build - karaoqueue-backend"
}
]
}

3
backend/.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"python.pythonPath": "pyenv/bin/python"
}

59
backend/.vscode/tasks.json vendored Normal file
View File

@ -0,0 +1,59 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "Launch MongoDB",
"type": "shell",
"command": "docker-compose -f ${workspaceFolder}/docker/docker-compose.dev.yml up",
"isBackground": true,
"problemMatcher": [
{
"pattern": [
{
"regexp": ".",
"file": 1,
"location": 2,
"message": 3
}
],
"background": {
"activeOnStart": true,
"beginsPattern": ".",
"endsPattern": "."
}
}
],
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "shared",
"showReuseMessage": true,
"clear": true
},
"group": "build"
},
{
"label": "Stop MongoDB",
"type": "shell",
"command": "docker-compose -f ${workspaceFolder}/docker/docker-compose.dev.yml stop"
},
{
"label": "Reset MongoDB",
"type": "shell",
"command": "docker-compose -f ${workspaceFolder}/docker/docker-compose.dev.yml rm -sf",
"problemMatcher": []
},
{
"type": "npm",
"script": "build",
"path": "karaoqueue-backend/",
"group": "build",
"problemMatcher": [],
"label": "npm: build - karaoqueue-backend",
"detail": "tsc"
}
]
}

View File

@ -1 +0,0 @@
web: gunicorn wsgi:app

View File

@ -1,259 +0,0 @@
from flask import Flask, render_template, Response, abort, request, redirect, send_from_directory
import helpers
import database
import data_adapters
import os
import json
from flask_basicauth import BasicAuth
from helpers import nocache
app = Flask(__name__, static_url_path='/static')
basic_auth = BasicAuth(app)
accept_entries = False
@app.route("/")
def home():
if basic_auth.authenticate():
return render_template('main_admin.html', list=database.get_list(), auth=basic_auth.authenticate())
else:
return render_template('main.html', list=database.get_list(), auth=basic_auth.authenticate())
@app.route("/favicon.ico")
def favicon():
return send_from_directory(os.path.join(app.root_path, 'static'),
'favicon.ico', mimetype='image/vnd.microsoft.icon')
@app.route('/api/enqueue', methods=['POST'])
@nocache
def enqueue():
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)
name = request.json['name']
song_id = request.json['id']
if request.authorization:
database.add_entry(name, song_id, client_id)
return Response('{"status":"OK"}', mimetype='text/json')
else:
if accept_entries:
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)
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')
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:
return Response('{"status":"Die Warteschlange enthält momentan ' + str(database.check_queue_length()) + ' Einträge und ist lang genug, bitte versuche es noch einmal wenn ein paar Songs gesungen wurden."}', mimetype='text/json', status=423)
else:
return Response('{"status":"Currently not accepting entries"}', mimetype='text/json', status=423)
@app.route("/list")
def songlist():
return render_template('songlist.html', list=database.get_song_list(), auth=basic_auth.authenticate())
@app.route("/settings")
@nocache
@basic_auth.required
def settings():
return render_template('settings.html', app=app, auth=basic_auth.authenticate())
@app.route("/settings", methods=['POST'])
@nocache
@basic_auth.required
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)
else:
abort(400)
if maxqueue.isnumeric and int(maxqueue) > 0:
app.config['MAX_QUEUE'] = int(maxqueue)
else:
abort(400)
return render_template('settings.html', app=app, auth=basic_auth.authenticate())
@app.route("/api/queue")
@nocache
def queue_json():
list = data_adapters.dict_from_rows(database.get_list())
return Response(json.dumps(list, ensure_ascii=False).encode('utf-8'), mimetype='text/json')
@app.route("/plays")
@nocache
@basic_auth.required
def played_list():
return render_template('played_list.html', list=database.get_played_list(), auth=basic_auth.authenticate())
@app.route("/api/songs")
@nocache
def songs():
list = database.get_song_list()
return Response(json.dumps(list, ensure_ascii=False).encode('utf-8'), mimetype='text/json')
@app.route("/api/songs/update")
@nocache
@basic_auth.required
def update_songs():
database.delete_all_entries()
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")
@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')
else:
return 400
@app.route("/api/entries/delete/<entry_id>")
@nocache
@basic_auth.required
def delete_entry(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
@basic_auth.required
def delete_entries():
if not request.json:
print(request.data)
abort(400)
return
updates = database.delete_entries(request.json)
if updates >= 0:
return Response('{"status": "OK", "updates": '+str(updates)+'}', mimetype='text/json')
else:
return Response('{"status": "FAIL"}', mimetype='text/json', status=400)
@app.route("/api/entries/mark_sung/<entry_id>")
@nocache
@basic_auth.required
def mark_sung(entry_id):
if database.add_sung_song(entry_id):
return Response('{"status": "OK"}', mimetype='text/json')
else:
return Response('{"status": "FAIL"}', mimetype='text/json')
@app.route("/api/entries/mark_transferred/<entry_id>")
@nocache
@basic_auth.required
def mark_transferred(entry_id):
if database.toggle_transferred(entry_id):
return Response('{"status": "OK"}', mimetype='text/json')
else:
return Response('{"status": "FAIL"}', mimetype='text/json')
@app.route("/api/entries/accept/<value>")
@nocache
@basic_auth.required
def set_accept_entries(value):
global accept_entries
if (value == '0' or value == '1'):
accept_entries = bool(int(value))
return Response('{"status": "OK"}', mimetype='text/json')
else:
return Response('{"status": "FAIL"}', mimetype='text/json', status=400)
@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')
@app.route("/api/played/clear")
@nocache
@basic_auth.required
def clear_played_songs():
if database.clear_played_songs():
return Response('{"status": "OK"}', mimetype='text/json')
else:
return Response('{"status": "FAIL"}', mimetype='text/json')
@app.route("/api/entries/delete_all")
@nocache
@basic_auth.required
def delete_all_entries():
if database.delete_all_entries():
return Response('{"status": "OK"}', mimetype='text/json')
else:
return Response('{"status": "FAIL"}', mimetype='text/json')
@app.route("/login")
@basic_auth.required
def admin():
return redirect("/", code=303)
@app.before_first_request
def activate_job():
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()
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'
return response
@app.context_processor
def inject_version():
return dict(karaoqueue_version=app.config['VERSION'])
if __name__ == "__main__":
app.run(host='127.0.0.1', port=8080, debug=True)

View File

@ -1,19 +0,0 @@
runtime: python39
manual_scaling:
# Das ist alles bloß dumm schnell zusammengehackt...
instances: 1
handlers:
# This configures Google App Engine to serve the files in the app's static
# directory.
- url: /static
static_dir: static
# This handler routes all requests not caught above to your main app. It is
# required when static routes are defined, but can be omitted (along with
# the entire handlers section) when there are no static files defined.
- url: /.*
script: auto
secure: always
redirect_http_response_code: 301

View File

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

View File

@ -1,212 +0,0 @@
# -*- coding: utf_8 -*-
import sqlite3
import pandas
from io import StringIO
song_table = "songs"
entry_table = "entries"
index_label = "Id"
done_table = "done_songs"
def open_db():
conn = sqlite3.connect("/tmp/karaoqueue.db")
conn.execute('PRAGMA encoding = "UTF-8";')
return conn
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()
print("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()
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()
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()
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
FROM entries, songs
WHERE entries.Song_Id=songs.Id""")
conn.close()
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
FROM songs, done_songs
WHERE done_songs.Song_Id=songs.Id""")
conn.close()
def get_list():
conn = open_db()
conn.row_factory = sqlite3.Row
cur = conn.cursor()
cur.execute("SELECT * FROM Liste")
return cur.fetchall()
def get_played_list():
conn = open_db()
cur = conn.cursor()
cur.execute("SELECT * FROM Abspielliste")
return cur.fetchall()
def get_song_list():
conn = open_db()
cur = conn.cursor()
cur.execute("SELECT Artist || \" - \" || Title AS Song, Id FROM songs;")
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()
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
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()
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()
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,))
return cur.fetchall()[0][0]
def check_queue_length():
conn = open_db()
cur = conn.cursor()
cur.execute("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()
return True
def delete_entry(id):
conn = open_db()
cur = conn.cursor()
cur.execute("DELETE FROM entries WHERE id=?", (id,))
conn.commit()
conn.close()
return True
def delete_entries(ids):
idlist = []
for x in ids:
idlist.append((x,))
try:
conn = open_db()
cur = conn.cursor()
cur.executemany("DELETE FROM entries WHERE id=?", idlist)
conn.commit()
conn.close()
return cur.rowcount
except sqlite3.Error as error:
return -1
def delete_all_entries():
conn = open_db()
cur = conn.cursor()
cur.execute("DELETE FROM entries")
conn.commit()
conn.close()
return True

View File

@ -0,0 +1,14 @@
version: '2'
services:
mongo:
extends:
file: docker-compose.yml
service: mongo
mongo-express:
depends_on:
- mongo
image: mongo-express
restart: always
ports:
- "8081:8081"

View File

@ -0,0 +1,12 @@
version: '2'
services:
mongo:
image: mongo
restart: always
ports:
- "27017:27017"
backend:
depends_on:
- mongo
build: .

View File

@ -1,79 +0,0 @@
import requests
from bs4 import BeautifulSoup
import json
import os
import uuid
from flask import make_response
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)
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']
return url
def get_songs(url):
r = requests.get(url)
return r.text
def is_valid_uuid(val):
try:
uuid.UUID(str(val))
return True
except ValueError:
return False
def check_config_exists():
return os.path.isfile(config_file)
def load_version(app):
if os.environ.get("SOURCE_VERSION"):
app.config['VERSION'] = os.environ.get("SOURCE_VERSION")[0:7]
elif os.path.isfile(".version"):
with open('.version', 'r') as file:
data = file.read().replace('\n', '')
if data:
app.config['VERSION'] = data
else:
app.config['VERSION'] = ""
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")
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")
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']
def nocache(view):
@wraps(view)
def no_cache(*args, **kwargs):
response = make_response(view(*args, **kwargs))
response.headers['Last-Modified'] = datetime.now()
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)

View File

@ -0,0 +1,10 @@
# Fixed login Username
KQUEUE_USERNAME=admin
# Fixed login Password
KQUEUE_PASSWORD=pass
# Port the app is listening on
KQUEUE_PORT=3000
# Secret used to sign JSON Web Tokens
KQUEUE_JWTSECRET=THIS_IS_A_BAD_SECRET_PLEASE_CHANGE
# Expiry time for the login jwt tokens in minutes
KQUEUE_JWTEXPIRY=1440 # 24h

117
backend/karaoqueue-backend/.gitignore vendored Normal file
View File

@ -0,0 +1,117 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,38 @@
{
"name": "karaoqueue-backend",
"version": "0.0.1",
"description": "Backend for KaraoQueue",
"main": "index.js",
"scripts": {
"start": "node dist/index.js",
"debug": "node --nolazy dist/index.js",
"build": "tsc",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Phillip Kühne",
"license": "ISC",
"devDependencies": {
"@types/body-parser": "^1.19.0",
"@types/debug": "^4.1.5",
"@types/express": "^4.17.6",
"@types/multer": "^1.4.3",
"@types/node": "^14.0.5",
"tslint": "^6.1.2",
"typescript": "^3.9.3"
},
"dependencies": {
"@types/mongodb": "^3.5.18",
"body-parser": "^1.19.0",
"class-transformer": "^0.3.1",
"class-validator": "^0.12.2",
"cors": "^2.8.5",
"debug": "^4.1.1",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"jsonwebtoken": "^8.5.1",
"mongodb": "^3.5.7",
"multer": "^1.4.2",
"reflect-metadata": "^0.1.13",
"routing-controllers": "^0.9.0-alpha.1"
}
}

View File

@ -0,0 +1,14 @@
import AppState from "../models/appState.model";
import fs from "fs";
const appState = new AppState();
if (fs.existsSync("/tmp/.kqueue_eventlock")) {
appState.currentlyInEvent=true;
} else {
appState.currentlyInEvent=false;
}
appState.registrationEnabled=false;
export default appState;

View File

@ -0,0 +1,34 @@
import { Request, Response } from "express";
import { Post, BodyParam, Body, Res, Req, JsonController, UseBefore, Get, CookieParam } from "routing-controllers";
import User from "../interfaces/user.interface";
import { JwtMiddleware } from "../middleware/jwt.middleware";
@JsonController("/auth")
export class AuthenticationController {
@Post("/login")
doLogin(@Body() user: User, @Res() res: Response) {
if (user.username === process.env.KQUEUE_USERNAME) {
if (user.password === process.env.KQUEUE_PASSWORD) {
const jwtMiddleware = new JwtMiddleware();
const tokenData = jwtMiddleware.createToken(user);
res.cookie("jwt",tokenData,);
res.status(200);
res.send("Welcome.")
return res;
} else {
res.status(401).send("Wrong user or password.");
return res;
}
} else {
res.status(401).send("Wrong user or password.");
return res;
}
}
/* TODO Logout with JWT? */
@Get("/logout")
doLogout() {
return "//TODO logout";
}
}

View File

@ -0,0 +1,76 @@
import { Request, Response } from "express";
import { Controller, Get, Res, Post, Delete, Patch, Req, Authorized, CurrentUser } from "routing-controllers";
import appState from "../containers/appState.container";
import DataStoredInToken from "../interfaces/dataStoredInToken.interface";
@Controller("/queue")
export class QueueController {
/*
* Fetch entry Queue content
*/
@Get()
getQueue(@Req() req: Request, @Res() res: Response) {
res.setHeader('Content-Type', 'application/json');
res.send(JSON.stringify({ placeholder: "//TODO fetch" }));
return res;
}
/*
* Add entry to Queue
*/
@Post()
addEntry(@Req() req: Request, @Res() res: Response) {
res.setHeader('Content-Type', 'application/json');
if (appState.registrationEnabled) {
res.send(JSON.stringify({ placeholder: "//TODO add" }));
} else {
res.status(403)
res.send("Entry submission is currently not allowed.")
}
return res;
}
/*
*
*/
@Delete()
@Authorized()
clearQueue(@Req() req: Request, @Res() res: Response) {
return "//TODO clear";
}
/*
*
*/
@Get("/:entry:id")
getEntry(@Req() req: Request, @Res() res: Response) {
res.setHeader('Content-Type', 'application/json');
res.send(JSON.stringify({ placeholder: "//TODO get" }));
return res;
}
/*
*
*/
@Patch("/:entry_id")
editEntry(@Req() req: Request, @Res() res: Response, @CurrentUser() user?: DataStoredInToken) {
if (user) {
return "You're "+user._id;
} else {
/*TODO: Require song-specific auth to modify queue entry */
return "You are not logged in."
}
}
@Delete("/:entry_id")
deleteEntry(@Req() req: Request, @Res() res: Response, @CurrentUser() user?: DataStoredInToken) {
if (user) {
return "You're " + user._id;
} else {
/*TODO: Require song-specific auth to delete queue entry */
return "You are not logged in."
}
}
}

View File

@ -0,0 +1,67 @@
import { Response } from "express";
import { Authorized, Controller, Get, Param, QueryParam, Res } from "routing-controllers";
import appState from "../containers/appState.container";
import fs from "fs";
@Controller("/rpc")
@Authorized()
export class RpcController {
@Get("/start_event")
doStartEvent() {
/* TODO: Wipe state (queue and song playbacks) when starting new event. Maybe also refresh Song database automatically? */
appState.currentlyInEvent = true;
fs.openSync("/tmp/.kqueue_eventlock", 'w')
return 200;
}
@Get("/end_event")
doEndEvent() {
appState.currentlyInEvent = false;
appState.registrationEnabled = false;
fs.unlinkSync("/tmp/.kqueue_eventlock");
return 200;
}
@Get("/enable_registration")
doEnableRegistration(@Res() res: Response) {
if (appState.currentlyInEvent) {
appState.registrationEnabled = true;
return 200;
} else {
res.status(403).send("No event currently active")
return res;
}
}
@Get("/disable_registration")
doDisableRegistration(@Res() res: Response) {
if (appState.currentlyInEvent) {
appState.registrationEnabled = false;
return 200;
} else {
res.status(403).send("No event currently active")
return res;
}
}
@Get("/get_state")
doGetState() {
return appState;
}
@Get("/get_playstats")
doGetPlaystats() {
return "//TODO get_playstats"
}
@Get("/download_playstats")
doDownloadPlaystats() {
return "//TODO download_playstats"
}
@Get("/entry_fulfilled")
doEntryFulfilled(@QueryParam("entry_id") entryId: string) {
return `//TODO entry_fulfilled. entry_id: ${entryId}`
}
}

View File

@ -0,0 +1,16 @@
import { Get, QueryParam, JsonController, Put, Authorized } from "routing-controllers";
@JsonController("/songs")
export class SongController {
@Get()
searchSongs(@QueryParam("query") query: string, @QueryParam("limit") limit: number) {
return {result: "//TODO search"}
}
@Put()
@Authorized()
updateSongs() {
return "//TODO update"
}
}

View File

@ -0,0 +1,9 @@
import { JsonController, Get } from "routing-controllers";
@JsonController()
export class StatisticsController {
@Get()
getStatistics() {
return "//TODO statistics"
}
}

View File

@ -0,0 +1,11 @@
class HttpException extends Error {
public status: number;
public message: string;
constructor(status: number, message: string) {
super(message);
this.status = status;
this.message = message;
}
}
export default HttpException;

View File

@ -0,0 +1,74 @@
import "reflect-metadata";
import { Request, Response, Application } from "express";
import { Action, createExpressServer } from "routing-controllers";
import { QueueController } from "./controllers/queue.controller";
import { SongController } from "./controllers/songs.controller";
import { StatisticsController } from "./controllers/statistics.controller";
import { AuthenticationController } from "./controllers/auth.controller";
import { RpcController } from "./controllers/rpc.controller";
import jwt from "jsonwebtoken";
import appState from "./containers/appState.container";
import * as dotenv from "dotenv";
import DataStoredInToken from "./interfaces/dataStoredInToken.interface";
dotenv.config();
const app: Application = createExpressServer({
routePrefix: "/api",
cors: true,
/* HACK. This definitely needs to be cleaned up... */
authorizationChecker: async (action: Action) => {
const req: Request = action.request;
const secret = process.env.KQUEUE_JWTSECRET;
// tslint:disable-next-line: no-string-literal
const token = parseCookies(req.headers.cookie)['jwt'];
if (token) {
try {
const verificationResponse = jwt.verify(token, secret);
if (verificationResponse) {
return true;
} else {
return false;
}
} catch (error) {
return false;
}
} else {
return false;
}
},
/* HACK. This definitely needs to be cleaned up... */
currentUserChecker: async (action: Action) => {
const req: Request = action.request;
const secret = process.env.KQUEUE_JWTSECRET;
// tslint:disable-next-line: no-string-literal
const token = parseCookies(req.headers.cookie)['jwt'];
if (token) {
try {
const verificationResponse = jwt.verify(token, secret);
if (verificationResponse) {
return verificationResponse as DataStoredInToken;
} else {
return false;
}
} catch (error) {
return false;
}
} else {
return false;
}
},
controllers: [QueueController, SongController, StatisticsController, AuthenticationController, RpcController]
});
app.listen(process.env.KQUEUE_PORT);
/* HACK. This definitely needs to be cleaned up... */
function parseCookies(str) {
const rx = /([^;=\s]*)=([^;]*)/g;
const obj = {};
// tslint:disable-next-line: no-conditional-assignment
for (let m; m = rx.exec(str);)
obj[m[1]] = decodeURIComponent(m[2]);
return obj;
}

View File

@ -0,0 +1,5 @@
interface DataStoredInToken {
_id: string;
}
export default DataStoredInToken;

View File

@ -0,0 +1,6 @@
interface User {
username: string;
password: string;
}
export default User;

View File

@ -0,0 +1,15 @@
import DataStoredInToken from "../interfaces/dataStoredInToken.interface";
import User from "../interfaces/user.interface";
import * as jwt from 'jsonwebtoken';
export class JwtMiddleware {
public createToken(user: User): string {
/* expiresIn is in seconds. We take the env value which is in minutes and multiply it by 60.*/
const expiresIn = parseInt(process.env.KQUEUE_JWTEXPIRY,10) * 60;
const secret = process.env.KQUEUE_JWTSECRET;
const dataStoredInToken: DataStoredInToken = {
_id: user.username,
};
return jwt.sign(dataStoredInToken, secret, { expiresIn });
}
}

View File

@ -0,0 +1,6 @@
class AppState {
registrationEnabled: boolean;
currentlyInEvent: boolean;
}
export default AppState;

View File

@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "es2017",
"module": "commonjs",
"outDir": "dist",
"sourceMap": true,
"esModuleInterop": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
},
"include": [
"src/**/*.ts"
],
"exclude": [
"node_modules"
]
}

View File

@ -0,0 +1,13 @@
{
"defaultSeverity": "error",
"extends": [
"tslint:recommended"
],
"jsRules": {},
"rules": {
"trailing-comma": [
false
]
},
"rulesDirectory": []
}

View File

@ -1,5 +0,0 @@
requests
pandas
Flask-BasicAuth
bs4
gunicorn

View File

@ -1,77 +0,0 @@
body {
padding-top: 5rem;
}
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: #f5f5f5;
}
.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;
}
@media (min-width: 768px) {
.topbutton {
width: auto;
}
}
@media print {
body {
font-size: 1.3em;
}
.footer {
display: none !important;
}
.admincontrols {
display: none;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 318 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

View File

@ -1,17 +0,0 @@
{
"name": "KaraoQueue",
"short_name": "KaraoQueue",
"start_url": "/",
"url": "https://karaoqueue-323511.appspot.com/",
"display": "standalone",
"background_color": "#343a40",
"description": "Eine Karaokewarteliste.",
"icons": [{
"src": "images/touch/homescreen512.png",
"sizes": "512x512",
"type": "image/png"
}],
"related_applications": [{
"platform": "Web"
}]
}

View File

@ -1,131 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<link rel="icon" href="favicon.ico">
<link rel="manifest" href="/static/manifest.webmanifest">
<title>{% block title %}{% endblock %} - KaraoQueue</title>
<!-- Bootstrap-Tables -->
<link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.15.3/dist/bootstrap-table.min.css">
<!-- Bootstrap core CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css"
integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
<!-- Custom styles for this template -->
<link href="static/css/style.css" rel="stylesheet">
<!-- Fontawesome Icons -->
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.2/css/all.css"
integrity="sha384-oS3vJWv+0UjzBfQzYUhtDYW+Pj2yciDJxpsK1OYPAYjqT085Qq/1cq5FLXAZQ7Ay" crossorigin="anonymous">
<!-- Bootstraptoggle -->
<link href="https://gitcdn.github.io/bootstrap-toggle/2.2.2/css/bootstrap-toggle.min.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-expand-md navbar-dark bg-dark fixed-top">
<a class="navbar-brand" href="/">KaraoQueue</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarsExampleDefault"
aria-controls="navbarsExampleDefault" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarsExampleDefault">
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<a class="nav-link" href="/">Warteliste</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/list">Songsuche</a>
</li>
{% if auth %}
<li class="nav-item">
<a class="nav-link" href="/plays">Abspielliste</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/settings">Einstellungen</a>
</li>
{% endif %}
</ul>
<!--<form class="form-inline my-2 my-lg-0">
<input class="form-control mr-sm-2" type="text" placeholder="Search" aria-label="Search">
<button class="btn btn-outline-success my-2 my-sm-0" type="submit">Search</button>
</form>-->
</div>
</nav>
<div class="site">
<main role="main" class="container">
{% block content %}{% endblock %}
</main><!-- /.container -->
</div>
<!-- Footer -->
<footer class="footer">
<div class="container text-center py-3">
{% if not auth %}
<a href="/login" class="ml-1 mr-1"><i class="fas fa-sign-in-alt mr-1"></i><span>Login</span></a>
{% endif %}
<!--<a href="https://github.com/PhoenixTwoFive/karaoqueue"
class="ml-1 mr-1"><i class="fab fa-github mr-1"></i><span>Github</span></a>-->
<span class="text-muted">KaraoQueue {{karaoqueue_version}} -&nbsp;<span>&copy</span>&nbsp;2019-21 - Phillip
Kühne</span>
</div>
</footer>
<!-- Footer -->
<!-- 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/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://cdnjs.cloudflare.com/ajax/libs/bootbox.js/4.4.0/bootbox.min.js"
integrity="sha256-4F7e4JsAJyLUdpP7Q8Sah866jCOhv72zU5E8lIRER4w=" crossorigin="anonymous">
</script>
<script src="https://unpkg.com/bootstrap-table@1.15.3/dist/bootstrap-table.min.js"></script>
<script
src="https://unpkg.com/bootstrap-table@1.15.3/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>
{% block extrajs %}{% endblock %}
<script>
$(document).ready(function () {
loadOrGenerateClientId()
// get current URL path and assign 'active' class
var pathname = window.location.pathname;
$('.navbar-nav > li > a[href="' + pathname + '"]').parent().addClass('active');
$('[data-toggle="tooltip"]').tooltip()
})
function create_UUID() {
var dt = new Date().getTime();
var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = (dt + Math.random() * 16) % 16 | 0;
dt = Math.floor(dt / 16);
return (c == 'x' ? r : (r & 0x3 | 0x8)).toString(16);
});
return uuid;
}
function loadOrGenerateClientId() {
if (!localStorage.getItem("clientId")) {
localStorage.setItem("clientId",create_UUID())
}
}
</script>
</body>
</html>

View File

@ -1,35 +0,0 @@
{% extends 'base.html' %}
{% block title %}Warteliste{% endblock %}
{% block content %}
<a id="bfb" role="button" class="btn btn-primary btn-lg btn-block mb-2" href="/list">Eintragen</a>
<table class="table entries"
data-toggle="table"
data-url="/api/queue"
data-pagination="true"
data-classes="table"
data-show-refresh="false"
data-auto-refresh="true"
data-auto-refresh-interval="10">
<thead>
<tr>
<th data-field="Name">Name</th>
<th data-field="Title">Song</th>
<th data-field="Artist">Künstler</th>
</tr>
</thead>
</table>
<a name="end"></a>
{% endblock %}
{% block extrajs %}
<script>
$.getJSON("/api/entries/accept", (data) => {
if (data["value"]==0) {
$("#bfb").addClass("disabled")
$("#bfb").prop("aria-disabled",true);
$("#bfb").prop("tabindex","-1");
}
})
</script>
{% endblock %}

View File

@ -1,212 +0,0 @@
{% extends 'base.html' %}
{% block title %}Warteliste-Admin{% endblock %}
{% block content %}
<style>
table td:nth-child(2) {
overflow-y: hidden;
overflow-x: auto;
text-overflow: clip;
max-width: 200px !important;
}
</style>
<div class="container">
<div id="toolbar">
<button type="button" class="topbutton btn btn-danger" onclick="confirmDeleteSelectedEntries()"><i
class="fas fa-trash mr-2"></i>Gewählte Einträge löschen</button>
<button type="button" class="topbutton btn btn-danger" onclick="confirmUpdateSongDatabase()"><i
class="fas fa-file-import mr-2"></i>Song-Datenbank
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">
</div>
<table class="table entries"
id="entrytable"
data-toggle="table"
data-search="true"
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>
<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-formatter="TableActions">Aktionen</th>
</tr>
</thead>
</table>
<a name="end"></a>
</div>
{% endblock %}
{% block extrajs %}
<script>
$(function () {
$('#entryToggle').change(function() {
$.ajax({url: "/api/entries/accept/"+($('#entryToggle').is(":checked") ? "1" : "0"), complete: setTimeout(refreshEntryToggle, 1000)});
})
refreshEntryToggle()
$("#entrytable").bootstrapTable().on('load-success.bs.table', function() {
$('[data-toggle="tooltip"]').tooltip()
})
})
function confirmDeleteEntry(name, entry_id) {
bootbox.confirm("Wirklich den Eintrag von "+name+" löschen?", function(result){
if (result) {
deleteEntry(entry_id)
}
})
}
function confirmDeleteSelectedEntries() {
bootbox.confirm({
message: "Wirklich gewählte Eintragungen löschen?",
buttons: {
confirm: {
label: 'Ja',
className: 'btn btn-danger'
},
cancel: {
label: 'Nein',
className: 'btn btn-secondary'
}
},
callback: function(result){
if (result) {
DeleteSelectedEntries(getIdSelections())
}
}
})
}
function confirmUpdateSongDatabase() {
bootbox.confirm({
message: "Wirklich die Song-Datenbank aktualisieren?<br>Dies lädt die Aktuelle Song-Liste von <a href='https://www.karafun.de/karaoke-song-list.html'>KaraFun</a> herunter, <b>und wird alle Eintragungen löschen!</b>",
buttons: {
confirm: {
label: 'Ja',
className: 'btn-primary'
},
cancel: {
label: 'Nein',
className: 'btn btn-secondary'
}
},
callback: function(result){
if (result) {
var dialog = bootbox.dialog({
message: '<p class="text-center mb-0"><i class="fa fa-spin fa-cog"></i> Aktualisiere Song-Datenbank...</p>',
closeButton: false
});
updateSongDatabase(dialog)
}
}
})
}
function refreshEntryToggle() {
$.getJSON("/api/entries/accept", (data) => {
if (data["value"]!=$('#entryToggle').is(":checked")) {
if(data["value"]==1) {
$('#entryToggle').data('bs.toggle').on('true')
}
else {
$('#entryToggle').data('bs.toggle').off('true')
}
}
})
}
function deleteEntry(entry_id) {
$.ajax({
type: 'GET',
url: '/api/entries/delete/'+entry_id,
contentType: "application/json",
dataType: 'json',
async: false
});
$("#entrytable").bootstrapTable('refresh')
}
function markEntryAsSung(entry_id) {
$.ajax({
type: 'GET',
url: '/api/entries/mark_sung/'+entry_id,
contentType: "application/json",
dataType: 'json',
async: false
});
$("#entrytable").bootstrapTable('refresh')
}
function markEntryAsTransferred(entry_id) {
$.ajax({
type: 'GET',
url: '/api/entries/mark_transferred/'+entry_id,
contentType: "application/json",
dataType: 'json',
async: false
});
$("#entrytable").bootstrapTable('refresh')
}
function DeleteSelectedEntries(ids) {
$.ajax({
type: 'POST',
url: '/api/entries/delete',
data: JSON.stringify(ids), // or JSON.stringify ({name: 'jonas'}),
error: function() {
bootbox.alert({
message: "Fehler beim Löschen der Eintragungen.",
})
},
success: function() {
$("#entrytable").bootstrapTable('refresh')
},
contentType: "application/json",
dataType: 'json'
});
}
function updateSongDatabase(wait_dialog) {
$.ajax({
type: 'GET',
url: '/api/songs/update',
contentType: "application/json",
dataType: 'json',
success: function(data) {
wait_dialog.modal('hide')
bootbox.alert({
message: data["status"],
callback: function() {
$("#entrytable").bootstrapTable('refresh')
}
})
}
});
}
function TableActions (value, row, index) {
let outerHTML = ""
if (row.Transferred==1) {
outerHTML = "<button type=\"button\" class=\"btn btn-default\" onclick=\"markEntryAsTransferred("+row.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.ID+")\"><i class=\"fas fa-check\"></i></button>&nbsp;<button type=\"button\" class=\"btn btn-danger\" data-toggle=\"tooltip\" data-placement=\"top\" title=\"Eintrag löschen\" onclick=\"confirmDeleteEntry('"+row.Name+"',"+row.ID+")\"><i class=\"fas fa-trash\"></i></button>";
} else {
outerHTML = "<button type=\"button\" class=\"btn btn-info\" onclick=\"markEntryAsTransferred("+row.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.ID+")\"><i class=\"fas fa-check\"></i></button>&nbsp;<button type=\"button\" class=\"btn btn-danger\" data-toggle=\"tooltip\" data-placement=\"top\" title=\"Eintrag löschen\" onclick=\"confirmDeleteEntry('"+row.Name+"',"+row.ID+")\"><i class=\"fas fa-trash\"></i></button>";
}
return outerHTML;
}
function getIdSelections() {
return $.map($("#entrytable").bootstrapTable('getSelections'), function (row) {
return row.ID
})
}
</script>
{% endblock %}

View File

@ -1,102 +0,0 @@
{% extends 'base.html' %}
{% block title %}Abspielliste{% endblock %}
{% block content %}
<div id="toolbar">
<button type="button" class="topbutton btn btn-danger" onclick="confirmDeleteAllEntries()"><i
class="fas fa-trash mr-2"></i>Abspielliste löschen</button>
<button type="button" class="topbutton btn btn-primary" onclick="exportPDF()"><i
class="fas fa-file-pdf mr-2"></i>Als PDF herunterladen</button>
<button type="button" class="topbutton btn btn-secondary" onclick="printPDF()"><i
class="fas fa-print mr-2"></i>Drucken</button>
</div>
<table class="table"
id="table"
data-toggle="table"
data-search="true"
data-show-columns="true"
data-toolbar="#toolbar"
data-pagination="true"
data-classes="table table-bordered table-striped"
data-show-extended-pagination="true">
<thead>
<tr>
<th scope="col">Song</th>
<th scope="col">Wiedergaben</th>
</tr>
</thead>
{% for entry in list: %}
<tr>
<td>
{{ entry[0] }}
</td>
<td>
{{ entry[1] }}
</td>
</tr>
{% endfor %}
</table>
</table>
{% endblock %}
{% block extrajs %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/1.5.3/jspdf.min.js"></script>
<script src="https://unpkg.com/jspdf-autotable@3.0.10/dist/jspdf.plugin.autotable.js"></script>
<script>
function confirmDeleteAllEntries() {
bootbox.confirm({
message: "Wirklich Abspielliste löschen?<br>Stelle sicher, dass du sie vorher zwecks Abrechnung gedruckt und/oder heruntergeladen hast!",
buttons: {
confirm: {
label: 'Ja',
className: 'btn btn-danger'
},
cancel: {
label: 'Nein',
className: 'btn btn-secondary'
}
},
callback: function(result){
if (result) {
deleteAllEntries()
}
}
})
}
function deleteAllEntries() {
$.ajax({
type: 'GET',
url: '/api/played/clear',
contentType: "application/json",
dataType: 'json',
async: false
});
location.reload();
}
function exportPDF() {
var doc = new jsPDF();
doc.autoTable({
head: [["Song","Wiedergaben"]],
body: createTableArray(),
theme: 'grid'
});
doc.save('Abspielliste.pdf');
}
function printPDF() {
var doc = new jsPDF();
doc.autoTable({
head: [["Song","Wiedergaben"]],
body: createTableArray(),
theme: 'grid'
});
doc.autoPrint();
doc.output('dataurlnewwindow');
}
function createTableArray() {
var data = $("#table").bootstrapTable('getData')
out = data.map(x => [x["0"],x["1"]])
return out;
}
</script>
{% endblock %}

View File

@ -1,18 +0,0 @@
{% extends 'base.html' %}
{% block title %}Einstellungen{% endblock %}
{% block content %}
<form method="post">
<p>
<label for="entryquota">Maximale Anzahl an Einträgen pro Nutzer</label>
<input type="number" class="form-control" id="entryquota" name="entryquota" min=1 value={{app.config['ENTRY_QUOTA']}}>
</p>
<p>
<label for="maxqueue">Maximale Anzahl an Einträgen Insgesamt</label>
<input type="number" class="form-control" id="maxqueue" name="maxqueue" min=1 value={{app.config['MAX_QUEUE']}}>
</p>
<input type="submit" class="btn btn-primary mr-1 mb-2" value="Einstellungen anwenden">
<input type="button" class="btn btn-default mr-1 mb-2" onclick="$.get('/writeSettings').done(()=>{alert('Einstellungen gespeichert')}).fail(()=>{alert('Fehler beim Speichern der Einstellungen')})" value="Einstellungen speichern"/>
</form>
{% endblock %}
{% block extrajs %}
{% endblock %}

View File

@ -1,136 +0,0 @@
{% extends 'base.html' %}
{% block title %}Songsuche{% endblock %}
{% block content %}
<input class="form-control" id="filter" type="text" placeholder="Suchen...">
<table class="table">
<tbody id="songtable">
</tbody>
</table>
<div class="modal fade" id="enqueueModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel"
aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Auf Liste setzen</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="form-group">
<form id="nameForm">
<div class="modal-body">
<label for="singerNameInput">Sängername</label>
<input type="text" class="form-control" id="singerNameInput" placeholder="Max Mustermann"
required>
<input id="selectedId" name="selectedId" type="hidden" value="">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary" id="submitSongButton">Anmelden</button>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extrajs %}
<script>
$(document).ready(function () {
$("#filter").focus();
$("#filter").keyup(function () {
var value = $(this).val().toLowerCase();
//alert(value);
if (value.length >= 1) {
$.getJSON("/api/songs/compl", { search: value }, function (data) {
var items = [];
$.each(data, function (key, val) {
items.push("<tr><td>" + val[0] + `</td>
<td><button type='button'
class='btn btn-primary justify-content-center align-content-between enqueueButton'
data-toggle='modal'
data-target='#enqueueModal' onclick='setSelectedId(`+ val[1] + `)'><i
class="fas fa-plus"></i></button></td>
</tr>`)
});
$("#songtable").html("")
$(items.join("")).appendTo("#songtable");
entriesAccepted()
});
} else {
$("#songtable").html("")
}
});
$("#nameForm").submit(function (e) {
e.preventDefault();
submitModal();
});
});
function enqueue(client_id, id, name, success_callback, blocked_callback) {
var data = {
"name": name,
"id": id,
"client_id": client_id
}
$.ajax({
type: 'POST',
url: '/api/enqueue',
data: JSON.stringify(data), // or JSON.stringify ({name: 'jonas'}),
success: success_callback,
statusCode: {
423: blocked_callback
},
contentType: "application/json",
dataType: 'json'
});
}
function setSelectedId(id) {
$("#selectedId").attr("value", id);
}
function submitModal() {
var name = $("#singerNameInput").val();
var id = $("#selectedId").attr("value");
enqueue(localStorage.getItem("clientId"),id, name, function () {
$("#enqueueModal").modal('hide');
window.location.href = '/#end';
}, function (response) {
bootbox.alert({
message: "Deine Eintragung konnte leider nicht vorgenommen werden.\nGrund: "+response.responseJSON.status,
});
entriesAccepted();
$("#enqueueModal").modal('hide');
});
}
{% if not auth %}
function entriesAccepted() {
$.getJSON("/api/entries/accept", (data, out) => {
if (data["value"] == 0) {
$(".enqueueButton").prop("disabled", true)
$(".enqueueButton").prop("style", "pointer-events: none;")
$(".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.")
$('[data-toggle="tooltip"]').tooltip()
} else {
$(".enqueueButton").prop("disabled", false)
}
})
}
{% else %}
function entriesAccepted() {
$(".enqueueButton").prop("disabled", false)
}
{% endif %}
</script>
{% endblock %}

View File

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

View File

@ -0,0 +1,13 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
max_line_length = off
trim_trailing_whitespace = false

46
frontend/ng-karaoqueue/.gitignore vendored Normal file
View File

@ -0,0 +1,46 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
/dist
/tmp
/out-tsc
# Only exists if Bazel was run
/bazel-out
# dependencies
/node_modules
# profiling files
chrome-profiler-events*.json
speed-measure-plugin*.json
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# misc
/.sass-cache
/connect.lock
/coverage
/libpeerconnection.log
npm-debug.log
yarn-error.log
testem.log
/typings
# System Files
.DS_Store
Thumbs.db

View File

@ -0,0 +1,27 @@
# NgKaraoqueue
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 8.3.20.
## Development server
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
## Code scaffolding
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
## Build
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build.
## Running unit tests
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Running end-to-end tests
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).

View File

@ -32,13 +32,7 @@
"src/custom-theme.scss",
"src/styles.scss"
],
"scripts": [],
"vendorChunk": true,
"extractLicenses": false,
"buildOptimizer": false,
"sourceMap": true,
"optimization": false,
"namedChunks": true
"scripts": []
},
"configurations": {
"production": {
@ -51,6 +45,7 @@
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
@ -69,8 +64,7 @@
}
]
}
},
"defaultConfiguration": ""
}
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
@ -112,6 +106,19 @@
"scripts": []
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"tsconfig.app.json",
"tsconfig.spec.json",
"e2e/tsconfig.json"
],
"exclude": [
"**/node_modules/**"
]
}
},
"e2e": {
"builder": "@angular-devkit/build-angular:protractor",
"options": {
@ -127,9 +134,10 @@
}
}
},
"defaultProject": "ng-karaoqueue",
"schematics": {
"@schematics/angular:component": {
"style": "scss"
"styleext": "scss"
}
}
}

View File

@ -0,0 +1,12 @@
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
# You can see what browsers were selected by your queries by running:
# npx browserslist
> 0.5%
last 2 versions
Firefox ESR
not dead
not IE 9-11 # For IE 9-11 support, remove 'not'.

View File

@ -0,0 +1,32 @@
// @ts-check
// Protractor configuration file, see link for more information
// https://github.com/angular/protractor/blob/master/lib/config.ts
const { SpecReporter } = require('jasmine-spec-reporter');
/**
* @type { import("protractor").Config }
*/
exports.config = {
allScriptsTimeout: 11000,
specs: [
'./src/**/*.e2e-spec.ts'
],
capabilities: {
browserName: 'chrome'
},
directConnect: true,
baseUrl: 'http://localhost:4200/',
framework: 'jasmine',
jasmineNodeOpts: {
showColors: true,
defaultTimeoutInterval: 30000,
print: function() {}
},
onPrepare() {
require('ts-node').register({
project: require('path').join(__dirname, './tsconfig.json')
});
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
}
};

View File

@ -0,0 +1,23 @@
import { AppPage } from './app.po';
import { browser, logging } from 'protractor';
describe('workspace-project App', () => {
let page: AppPage;
beforeEach(() => {
page = new AppPage();
});
it('should display welcome message', () => {
page.navigateTo();
expect(page.getTitleText()).toEqual('ng-karaoqueue app is running!');
});
afterEach(async () => {
// Assert that there are no errors emitted from the browser
const logs = await browser.manage().logs().get(logging.Type.BROWSER);
expect(logs).not.toContain(jasmine.objectContaining({
level: logging.Level.SEVERE,
} as logging.Entry));
});
});

View File

@ -0,0 +1,11 @@
import { browser, by, element } from 'protractor';
export class AppPage {
navigateTo() {
return browser.get(browser.baseUrl) as Promise<any>;
}
getTitleText() {
return element(by.css('app-root .content span')).getText() as Promise<string>;
}
}

View File

@ -0,0 +1,13 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/e2e",
"module": "commonjs",
"target": "es5",
"types": [
"jasmine",
"jasminewd2",
"node"
]
}
}

View File

@ -0,0 +1,32 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, './coverage/ng-karaoqueue'),
reports: ['html', 'lcovonly', 'text-summary'],
fixWebpackSourcePaths: true
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false,
restartOnFileChange: true
});
};

File diff suppressed because it is too large Load Diff

View File

@ -12,40 +12,43 @@
},
"private": true,
"dependencies": {
"@angular/animations": "^14.2.3",
"@angular/cdk": "^14.2.2",
"@angular/common": "^14.2.3",
"@angular/compiler": "^14.2.3",
"@angular/core": "^14.2.3",
"@angular/forms": "^14.2.3",
"@angular/material": "^14.2.2",
"@angular/platform-browser": "^14.2.3",
"@angular/platform-browser-dynamic": "^14.2.3",
"@angular/router": "^14.2.3",
"@mdi/angular-material": "^7.0.96",
"@angular/animations": "~9.0.5",
"@angular/cdk": "~9.1.2",
"@angular/common": "~9.0.5",
"@angular/compiler": "~9.0.5",
"@angular/core": "~9.0.5",
"@angular/forms": "~9.0.5",
"@angular/material": "^9.1.2",
"@angular/platform-browser": "~9.0.5",
"@angular/platform-browser-dynamic": "~9.0.5",
"@angular/router": "~9.0.5",
"@mdi/angular-material": "^5.0.45",
"@ngx-pwa/local-storage": "^9.0.3",
"hammerjs": "^2.0.8",
"rxjs": "~7.5.6",
"tslib": "^2.4.0",
"zone.js": "~0.11.8"
"runtime-config-loader": "^2.0.0",
"rxjs": "~6.5.4",
"tslib": "^1.11.1",
"zone.js": "~0.10.2"
},
"devDependencies": {
"@angular/cli": "^14.2.3",
"@angular/compiler-cli": "^14.2.3",
"@angular/language-service": "^14.2.3",
"@types/jasmine": "~4.3.0",
"@types/jasminewd2": "~2.0.10",
"@types/node": "~18.7.19",
"codelyzer": "^0.0.28",
"jasmine-core": "~4.4.0",
"jasmine-spec-reporter": "~7.0.0",
"karma": "~6.4.1",
"karma-chrome-launcher": "~3.1.1",
"karma-coverage-istanbul-reporter": "~3.0.3",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "^2.0.0",
"protractor": "~7.0.0",
"ts-node": "~10.9.1",
"tslint": "~6.1.0",
"typescript": "~4.8.3"
"@angular-devkit/build-angular": "^0.900.5",
"@angular/cli": "~9.0.5",
"@angular/compiler-cli": "~9.0.5",
"@angular/language-service": "~9.0.5",
"@types/jasmine": "~3.5.9",
"@types/jasminewd2": "~2.0.8",
"@types/node": "~13.9.0",
"codelyzer": "^5.2.1",
"jasmine-core": "~3.5.0",
"jasmine-spec-reporter": "~4.2.1",
"karma": "~4.4.1",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage-istanbul-reporter": "~2.1.1",
"karma-jasmine": "~3.1.1",
"karma-jasmine-html-reporter": "^1.5.2",
"protractor": "~5.4.3",
"ts-node": "~8.6.2",
"tslint": "~6.0.0",
"typescript": "~3.8.3"
}
}

View File

@ -0,0 +1,35 @@
import { TestBed, async } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule
],
declarations: [
AppComponent
],
}).compileComponents();
}));
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy();
});
it(`should have as title 'ng-karaoqueue'`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app.title).toEqual('ng-karaoqueue');
});
it('should render title', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector('.content span').textContent).toContain('ng-karaoqueue app is running!');
});
});

View File

@ -11,6 +11,6 @@ export class Genre {
}
id: number;
id: number;
_name: string;
}

View File

@ -3,7 +3,7 @@
* https://materialtheme.arcsine.dev
*/
@import '@angular/material/theming';
@import '~@angular/material/theming';
// Include the common styles for Angular Material. We include this here so that you only
// have to load a single css file for Angular Material in your app.

View File

@ -0,0 +1,3 @@
export const environment = {
production: true
};

View File

@ -0,0 +1,16 @@
// This file can be replaced during build by using the `fileReplacements` array.
// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
// The list of file replacements can be found in `angular.json`.
export const environment = {
production: false
};
/*
* For easier debugging in development mode, you can import the following file
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
*
* This import should be commented out in production mode because it will have a negative impact
* on performance if an error is thrown.
*/
// import 'zone.js/dist/zone-error'; // Included with Angular CLI.

Binary file not shown.

After

Width:  |  Height:  |  Size: 948 B

View File

@ -0,0 +1,13 @@
import 'hammerjs';
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));

View File

@ -0,0 +1,63 @@
/**
* This file includes polyfills needed by Angular and is loaded before the app.
* You can add your own extra polyfills to this file.
*
* This file is divided into 2 sections:
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
* file.
*
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
*
* Learn more in https://angular.io/guide/browser-support
*/
/***************************************************************************************************
* BROWSER POLYFILLS
*/
/** IE10 and IE11 requires the following for NgClass support on SVG elements */
// import 'classlist.js'; // Run `npm install --save classlist.js`.
/**
* Web Animations `@angular/platform-browser/animations`
* Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
* Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
*/
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
/**
* By default, zone.js will patch all possible macroTask and DomEvents
* user can disable parts of macroTask/DomEvents patch by setting following flags
* because those flags need to be set before `zone.js` being loaded, and webpack
* will put import in the top of bundle, so user need to create a separate file
* in this directory (for example: zone-flags.ts), and put the following flags
* into that file, and then add the following code before importing zone.js.
* import './zone-flags.ts';
*
* The flags allowed in zone-flags.ts are listed here.
*
* The following flags will work for all browsers.
*
* (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
* (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
* (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
*
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
* with the following flag, it will bypass `zone.js` patch for IE/Edge
*
* (window as any).__Zone_enable_cross_context_check = true;
*
*/
/***************************************************************************************************
* Zone JS is required by default for Angular itself.
*/
import 'zone.js/dist/zone'; // Included with Angular CLI.
/***************************************************************************************************
* APPLICATION IMPORTS
*/

View File

@ -0,0 +1,20 @@
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
import 'zone.js/dist/zone-testing';
import { getTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
declare const require: any;
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting()
);
// Then we find all the tests.
const context = require.context('./', true, /\.spec\.ts$/);
// And load the modules.
context.keys().map(context);

View File

@ -1,4 +1,3 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
@ -6,9 +5,14 @@
"types": []
},
"files": [
"src/app/app.module.ts",
"src/main.ts",
"src/polyfills.ts"
],
"include": [
"src/**/*.d.ts"
"src/**/*.ts"
],
"exclude": [
"src/test.ts",
"src/**/*.spec.ts"
]
}
}

View File

@ -1,32 +1,27 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"compileOnSave": false,
"compilerOptions": {
"baseUrl": "./",
"outDir": "./dist/out-tsc",
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"sourceMap": true,
"declaration": false,
"downlevelIteration": true,
"experimentalDecorators": true,
"moduleResolution": "node",
"importHelpers": true,
"target": "es2020",
"module": "es2020",
"lib": [
"es2020",
"dom"
]
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
}
"compileOnSave": false,
"compilerOptions": {
"baseUrl": "./",
"outDir": "./dist/out-tsc",
"sourceMap": true,
"declaration": false,
"downlevelIteration": true,
"experimentalDecorators": true,
"module": "esnext",
"moduleResolution": "node",
"importHelpers": true,
"target": "es2015",
"typeRoots": [
"node_modules/@types"
],
"lib": [
"es2018",
"dom"
]
},
"angularCompilerOptions": {
"fullTemplateTypeCheck": true,
"strictInjectionParameters": true,
"disableTypeScriptVersionCheck": true
}
}

View File

@ -1,10 +1,10 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": [
"jasmine"
"jasmine",
"node"
]
},
"files": [

View File

@ -0,0 +1,91 @@
{
"extends": "tslint:recommended",
"rules": {
"array-type": false,
"arrow-parens": false,
"deprecation": {
"severity": "warning"
},
"component-class-suffix": true,
"contextual-lifecycle": true,
"directive-class-suffix": true,
"directive-selector": [
true,
"attribute",
"app",
"camelCase"
],
"component-selector": [
true,
"element",
"app",
"kebab-case"
],
"import-blacklist": [
true,
"rxjs/Rx"
],
"interface-name": false,
"max-classes-per-file": false,
"max-line-length": [
true,
140
],
"member-access": false,
"member-ordering": [
true,
{
"order": [
"static-field",
"instance-field",
"static-method",
"instance-method"
]
}
],
"no-consecutive-blank-lines": false,
"no-console": [
true,
"debug",
"info",
"time",
"timeEnd",
"trace"
],
"no-empty": false,
"no-inferrable-types": [
true,
"ignore-params"
],
"no-non-null-assertion": true,
"no-redundant-jsdoc": true,
"no-switch-case-fall-through": true,
"no-var-requires": false,
"object-literal-key-quotes": [
true,
"as-needed"
],
"object-literal-sort-keys": false,
"ordered-imports": false,
"quotemark": [
true,
"single"
],
"trailing-comma": false,
"no-conflicting-lifecycle": true,
"no-host-metadata-property": true,
"no-input-rename": true,
"no-inputs-metadata-property": true,
"no-output-native": true,
"no-output-on-prefix": true,
"no-output-rename": true,
"no-outputs-metadata-property": true,
"template-banana-in-box": true,
"template-no-negated-async": true,
"use-lifecycle-interface": true,
"use-pipe-transform-interface": true
},
"rulesDirectory": [
"codelyzer"
]
}