frontend, etc.

This commit is contained in:
2024-05-17 09:41:26 +02:00
parent 2185b217e1
commit c1c9f98e87
19 changed files with 573 additions and 17 deletions

View File

@@ -1,6 +1,11 @@
import sqlalchemy import sqlalchemy
from sqlalchemy.ext.automap import automap_base
from sqlalchemy.orm import Session
Base = automap_base()
dbEngine = sqlalchemy.create_engine('sqlite:///db.sqlite') dbEngine = sqlalchemy.create_engine('sqlite:///db.sqlite')
def get_token_header(): dbSession = Session(dbEngine)
pass
Base.prepare(dbEngine, reflect=True)

View File

@@ -1,16 +1,30 @@
from fastapi import FastAPI from fastapi import FastAPI, Request
from app.routers import admin, user from app.routers import admin, user, songs
from fastapi.staticfiles import StaticFiles
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
app = FastAPI() app = FastAPI()
app.include_router(admin.router) app.include_router(admin.router)
app.include_router(user.router) app.include_router(user.router)
app.include_router(songs.router)
@app.get("/") app.mount("/static", StaticFiles(directory="static"), name="static")
def root():
return {"message": "Hello World"}
templates = Jinja2Templates(directory="templates")
@app.get("/", response_class=HTMLResponse)
async def root(request: Request, session_id : str = ""):
if session_id == "":
return templates.TemplateResponse(
request=request, name="landing.html"
)
else:
return templates.TemplateResponse(
request=request, name="voting.html"
)
# 1PMy17eraogNUz436w3aZKxyij39G1didaN02Ka_-45Q # 1PMy17eraogNUz436w3aZKxyij39G1didaN02Ka_-45Q
# 71046222 # 71046222

View File

@@ -1,8 +1,7 @@
from fastapi import APIRouter, Depends, HTTPException, Security from fastapi import APIRouter, HTTPException, Security, File, UploadFile
from app.models import Genre, Song, GoogleFile from app.models import GoogleFile
from app.dependencies import get_token_header, dbEngine from app.dependencies import dbEngine
from app.routers.user import get_current_user, User from app.routers.user import get_current_user
from typing import Annotated
import gspread import gspread
from gspread.urls import DRIVE_FILES_API_V3_URL from gspread.urls import DRIVE_FILES_API_V3_URL
import pandas as pd import pandas as pd
@@ -93,3 +92,8 @@ def process_worksheets():
song_list.to_sql(name='songs', con=dbEngine, song_list.to_sql(name='songs', con=dbEngine,
index=False, if_exists='append') index=False, if_exists='append')
# song_list.to_csv("song-list.csv") # song_list.to_csv("song-list.csv")
@router.post("/process_file")
async def create_upload_file(file: UploadFile):
return {"filename": file.filename}

16
app/routers/songs.py Normal file
View File

@@ -0,0 +1,16 @@
from fastapi import APIRouter, HTTPException, Security
from app.models import Song
from app.dependencies import dbEngine, Base, dbSession
from app.routers.user import get_current_user, User
from typing import Annotated
router = APIRouter(
prefix="/songs",
#dependencies=[Security(get_current_user, scopes=["public"])],
responses={404: {"description": "Not found"}},
)
@router.get("/")
async def get_songs() -> list[dict]:
return dbSession.query(Base.songs).all()

View File

@@ -12,7 +12,7 @@ from app.secrets import SECRET_KEY, fake_users_db
# openssl rand -hex 32 # openssl rand -hex 32
ALGORITHM = "HS256" ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30 ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 * 31
class Token(BaseModel): class Token(BaseModel):
@@ -42,7 +42,8 @@ pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer( oauth2_scheme = OAuth2PasswordBearer(
tokenUrl="user/token", tokenUrl="user/token",
scopes={ scopes={
"admin": "Perform admin actions." "admin": "Perform admin actions.",
"public": "Perform public actions."
} }
) )
@@ -65,6 +66,7 @@ def get_user(db, username: str):
return UserInDB(**user_dict) return UserInDB(**user_dict)
def authenticate_user(fake_db, username: str, password: str): def authenticate_user(fake_db, username: str, password: str):
user = get_user(fake_db, username) user = get_user(fake_db, username)
if not user: if not user:
@@ -84,7 +86,6 @@ def create_access_token(data: dict, expires_delta: timedelta | None = None):
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt return encoded_jwt
async def get_current_user( async def get_current_user(
security_scopes: SecurityScopes, token: Annotated[str, Depends(oauth2_scheme)] security_scopes: SecurityScopes, token: Annotated[str, Depends(oauth2_scheme)]
): ):
@@ -144,3 +145,11 @@ async def login_for_access_token(
data={"sub": user.username, "scopes": user.scopes}, expires_delta=access_token_expires data={"sub": user.username, "scopes": user.scopes}, expires_delta=access_token_expires
) )
return Token(access_token=access_token, token_type="bearer") return Token(access_token=access_token, token_type="bearer")
# @router.get("/public_token")
# async def get_public_access_token(secret_identity : str) -> Token:
# access_token_expires = timedelta(minutes=60*24*365)
# access_token = create_access_token(
# data={"sub": "public", "secret_identity" : secret_identity, "scopes": ["public"]}, expires_delta=access_token_expires
# )
# return Token(access_token=access_token, token_type="bearer")

View File

@@ -7,3 +7,4 @@ sqlalchemy
python-jose[cryptography] python-jose[cryptography]
passlib[bcrypt] passlib[bcrypt]
python-multipart python-multipart
jinja2

90
static/colors.css Normal file
View File

@@ -0,0 +1,90 @@
.cat-1 {
background-color: #1f77b4;
}
.cat-2 {
background-color: #ff7f0e;
}
.cat-3 {
background-color: #2ca02c;
}
.cat-4 {
background-color: #d62728;
}
.cat-5 {
background-color: #9467bd;
}
.cat-6 {
background-color: #8c564b;
}
.cat-7 {
background-color: #e377c2;
}
.cat-8 {
background-color: #7f7f7f;
}
.cat-9 {
background-color: #bcbd22;
}
.cat-10 {
background-color: #17becf;
}
.cat-11 {
background-color: #a1c9f4;
}
.cat-12 {
background-color: #ffb482;
}
.cat-13 {
background-color: #8de5a1;
}
.cat-14 {
background-color: #ff9f9b;
}
.cat-15 {
background-color: #d0bbff;
}
.cat-16 {
background-color: #debb9b;
}
.cat-17 {
background-color: #fab0e4;
}
.cat-18 {
background-color: #cfcfcf;
}
.cat-19 {
background-color: #fffea3;
}
.cat-20 {
background-color: #b9f2f0;
}
.cat-21 {
background-color: #001c7f;
}
.cat-22 {
background-color: #b1400d;
}
.cat-23 {
background-color: #12711c;
}
.cat-24 {
background-color: #8c0800;
}
.cat-25 {
background-color: #591e71;
}
.cat-26 {
background-color: #592f0d;
}
.cat-27 {
background-color: #a23582;
}
.cat-28 {
background-color: #3c3c3c;
}
.cat-29 {
background-color: #b8850a;
}
.cat-30 {
background-color: #006374;
}

BIN
static/cover.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

15
static/index.html Normal file
View File

@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<title>Hello jQuery</title>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<script src="hello.js"></script>
</head>
<body>
<div>
<p class="greeting-id">The ID is </p>
<p class="greeting-content">The content is </p>
</div>
</body>
</html>

8
static/landing_script.js Normal file
View File

@@ -0,0 +1,8 @@
$(document).ready(function () {
var s_id = localStorage.getItem('session_id')
if (s_id === null) {
s_id = window.crypto.randomUUID();
localStorage.setItem('session_id', s_id)
}
$('.vote-from-existing').attr('href', '?session_id=' + s_id);
});

22
static/neutral.svg Normal file
View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg
width="326.09866"
height="533.33331"
viewBox="0 0 9.7829599 15.999999"
fill="none"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<path
d="m 4.78296,15 h 0.01 M 1,3.69689 C 1.54049,2.12753 3.03006,1 4.78296,1 c 2.2091,0 4,1.79086 4,4 0,1.6565 -1.0069,3.0778 -2.442,3.6852 -0.7408,0.3136 -1.1112,0.4704 -1.2408,0.5915 -0.1543,0.1442 -0.1836,0.1884 -0.2562,0.3867 -0.061,0.1665 -0.061,0.4232 -0.061,0.9366 V 12"
stroke="#000000"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
id="path1" />
</svg>

After

Width:  |  Height:  |  Size: 789 B

22
static/no.svg Normal file
View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg
width="466.66666"
height="466.66666"
viewBox="0 0 14 14"
fill="none"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<path
d="M 1,1 13,13 M 13,1 1,13"
stroke="#000000"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
id="path1" />
</svg>

After

Width:  |  Height:  |  Size: 529 B

23
static/play.svg Normal file
View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg
width="259.76822"
height="419.52487"
viewBox="0 0 7.7930466 12.585746"
fill="none"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<path
d="m 1.0001448,2.2242414 v 8.1372996 c 0,0.6058 0,0.9087 0.1198,1.0489 0.10394,0.1218 0.25987,0.1863 0.41943,0.1738 0.18389,-0.0145 0.39808,-0.2287 0.82647,-0.6571 l 4.0686,-4.0685996 c 0.198,-0.198 0.297,-0.297 0.3341,-0.4112 0.0327,-0.1004 0.0327,-0.2086 0,-0.309 -0.0371,-0.1141 -0.1361,-0.2131 -0.3341,-0.4112 l -4.0686,-4.06859 c -0.42839,-0.42837 -0.64258,-0.64256 -0.82647,-0.65703 -0.15956,-0.01256 -0.31549,0.05203 -0.41943,0.17373 -0.1198,0.14026 -0.1198,0.44317 -0.1198,1.04899 z"
stroke="#000000"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
id="path1"
style="stroke:#000000;stroke-opacity:1" />
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

184
static/site.css Normal file
View File

@@ -0,0 +1,184 @@
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
padding: 0.5em;
}
@media only screen and (min-resolution: 200dpi) {
body {
font-size: 3.2vmin;
}
}
.clear {
clear: both;
}
.song {
background-color: #f0f0f0;
padding: 0.4em;
border-radius: 0.5em;
width: 30em;
font-family: sans-serif;
margin-bottom: 1em;
}
.cover-container {
position: relative;
width: 10.67em;
height: 6em;
float: left;
margin-right: 1em;
}
.cover {
display: block;
width: 100%;
height: auto;
}
.overlay {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
height: 100%;
width: 100%;
opacity: 0;
transition: .3s ease;
}
.cover-container:hover {
cursor: pointer;
}
.cover-container:hover .overlay {
opacity: 1;
filter: drop-shadow(0px 0px 1em black);
}
.overlay img {
height: 50%;
position: absolute;
top: 50%;
left: 50%;
-webkit-transform: translate(-50%, -50%);
-ms-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
filter: invert();
transition: .1s ease;
}
.cover-container:active .overlay img {
height: 60%;
}
.vote-buttons {
display: inline-block;
}
.button {
height: 2em;
width: 3em;
display: inline-block;
text-align: center;
vertical-align: middle;
font-size: 1.5em;
position: relative;
}
.button img {
height: 30%;
position: absolute;
top: 50%;
left: 50%;
-webkit-transform: translate(-50%, -50%);
-ms-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
transition: .1s ease;
}
.button:active img {
height: 40%;
}
.button-no {
background-color: #e1412f;
border-top-left-radius: 1em;
border-bottom-left-radius: 1em;
}
.button-neutral {
background-color: #f5bb00;
margin-left: 2px;
margin-right: 2px;
}
.button-yes {
background-color: #48a84f;
border-top-right-radius: 1em;
border-bottom-right-radius: 1em;
}
.button:not(.selected):not(:hover) {
background-color: #b0b0b0;
}
.button:hover {
filter: drop-shadow(2px 2px 2px) brightness(95%);
cursor: pointer;
}
.button span {
position: relative;
}
.button-no span {
top: 10%;
}
.button-yes span {
top: 10%;
}
.button-neutral span {
top: 20%;
}
.categories {
width: 60%;
overflow: hidden;
cursor: grab;
white-space: nowrap;
font-size: 0.7em;
line-height: 1.2em;
margin-top: 0.2em;
}
.categories span {
border-radius: 1.2em;
padding: 0 0.5em 0 0.5em;
margin-right: 0.4em;
display: inline-block;
color: white;
max-width: 10em;
overflow: clip;
}
.vote-buttons {
margin-top: 0.5em;
}
.song-data {
width: 100%;
overflow: clip;
white-space: nowrap;
}

70
static/voting_script.js Normal file
View File

@@ -0,0 +1,70 @@
document.addEventListener('DOMContentLoaded', function () {
const eles = document.getElementsByClassName('categories');
Array.prototype.forEach.call(eles, ele => {
//ele.style.cursor = 'grab';
let pos = { top: 0, left: 0, x: 0, y: 0 };
const mouseDownHandler = function (e) {
ele.style.cursor = 'grabbing';
ele.style.userSelect = 'none';
pos = {
left: ele.scrollLeft,
top: ele.scrollTop,
// Get the current mouse position
x: e.clientX,
y: e.clientY,
};
document.addEventListener('mousemove', mouseMoveHandler);
document.addEventListener('mouseup', mouseUpHandler);
};
const mouseMoveHandler = function (e) {
// How far the mouse has been moved
const dx = e.clientX - pos.x;
const dy = e.clientY - pos.y;
// Scroll the element
ele.scrollTop = pos.top - dy;
ele.scrollLeft = pos.left - dx;
};
const mouseUpHandler = function () {
ele.style.cursor = 'grab';
ele.style.removeProperty('user-select');
document.removeEventListener('mousemove', mouseMoveHandler);
document.removeEventListener('mouseup', mouseUpHandler);
};
// Attach the handler
ele.addEventListener('mousedown', mouseDownHandler);
});
});
const getQueryParameter = (param) => new URLSearchParams(document.location.search.substring(1)).get(param);
$(document).ready(function () {
var session_id = getQueryParameter("session_id");
$(".greeting-id").append(session_id);
$(".greeting-id").append("Foo");
$.ajax({
url: "/songs"
}).then(function (user_list) {
$('.greeting-id').append(user_list.total);
localStorage.setItem("test-storage", user_list.total);
var users = [];
$.each(user_list.data, function (key, user) {
users.push("<li id='" + user.id + "'>" + user.first_name + "</li>");
});
$("<ul/>", {
"class": "my-new-list",
html: users.join("")
}).appendTo("body");
});
});

22
static/yes.svg Normal file
View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg
width="600"
height="433.33197"
viewBox="0 0 18 12.999959"
fill="none"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<path
d="M 1,7.1111 5.92308,12 17,1"
stroke="#000000"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
id="path1" />
</svg>

After

Width:  |  Height:  |  Size: 533 B

9
templates/item.html Normal file
View File

@@ -0,0 +1,9 @@
<html>
<head>
<title>Item Details</title>
<link href="{{ url_for('static', path='/styles.css') }}" rel="stylesheet">
</head>
<body>
<h1><a href="test.html">Item ID: {{ id }}</a></h1>
</body>
</html>

15
templates/landing.html Normal file
View File

@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<title>Liederwahl</title>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<script src="{{ url_for('static', path='/landing_script.js') }}"></script>
</head>
<body>
<div>
<p><a href="" class="vote-from-existing">Abstimmen</a></p>
<!--<p>Fange eine neue Abstimmung an<a href="">Abstimmen</a></p>-->
</div>
</body>
</html>

27
templates/voting.html Normal file
View File

@@ -0,0 +1,27 @@
<!DOCTYPE html>
<html>
<head>
<title>Liederwahl</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', path='/colors.css') }}">
<link rel="stylesheet" type="text/css" href="{{ url_for('static', path='/site.css') }}">
<script src="{{ url_for('static', path='/voting_script.js') }}"></script>
</head>
<body>
<div class="song">
<div class="cover-container">
<img src="{{ url_for('static', path='/cover.jpg') }}" class="cover">
<div class="overlay"><img src="{{ url_for('static', path='/play.svg') }}"></div>
</div>
<div class="song-data">VoicePlay: In The Air Tonight</div>
<div class="categories" id="container">
<span class="cat-1">Ballade</span><span class="cat-2">&lt; 90er Remake</span><span class="cat-3">Something else </span><span class="cat-4">Something else </span>
</div>
<div class="vote-buttons">
<div class="button button-no"><img src="{{ url_for('static', path='/no.svg') }}"></div><div class="button button-neutral"><img src="{{ url_for('static', path='/neutral.svg') }}"></div><div class="button button-yes selected"><img src="{{ url_for('static', path='/yes.svg') }}"></div>
</div>
<div class="clear"></div>
</div>
</body>
</html>