Implement Veto Mode

This commit is contained in:
matthias@matsewe.de
2024-05-29 10:33:42 +02:00
parent 010d6fc8d6
commit 0546a88e32
9 changed files with 150 additions and 24 deletions

View File

@@ -10,9 +10,7 @@ def get_songs_and_vote_for_session(db, session_name) -> list[models.Song]:
models.Vote.session_id == session_entry.id).subquery() models.Vote.session_id == session_entry.id).subquery()
songs_and_votes = db.query( songs_and_votes = db.query(
models.Song, votes.c.vote models.Song, votes.c.vote, votes.c.comment
).filter(
models.Song.singable == True
).join(votes, isouter=True).filter().all() ).join(votes, isouter=True).filter().all()
return songs_and_votes return songs_and_votes
@@ -46,7 +44,8 @@ def create_song(db,
arng_url, arng_url,
categories, categories,
main_category, main_category,
singable singable,
comment
): ):
s = models.Song(og_artist=og_artist, s = models.Song(og_artist=og_artist,
aca_artist=aca_artist, aca_artist=aca_artist,
@@ -61,7 +60,8 @@ def create_song(db,
arng_url=arng_url, arng_url=arng_url,
categories=categories, categories=categories,
main_category=main_category, main_category=main_category,
singable=singable) singable=singable,
comment=comment)
db.add(s) db.add(s)
db.commit() db.commit()
@@ -80,6 +80,21 @@ def create_or_update_vote(db, song_id, session_name, vote):
db.add(vote_entry) db.add(vote_entry)
db.commit() db.commit()
def create_or_update_comment(db, song_id, session_name, comment):
session_entry = activate_session(db, session_name)
if comment == "":
comment = None
vote_entry = db.query(models.Vote).filter(
(models.Vote.session_id == session_entry.id) & (models.Vote.song_id == song_id)).first()
if vote_entry:
vote_entry.comment = comment # type: ignore
else:
vote_entry = models.Vote(
song_id=song_id, session_id=session_entry.id, comment=comment)
db.add(vote_entry)
db.commit()
def activate_session(db, session_name): def activate_session(db, session_name):
session_entry = db.query(models.Session).filter( session_entry = db.query(models.Session).filter(
@@ -104,3 +119,20 @@ def deactivate_session(db, session_name):
session_entry = models.Session(session_name=session_name, active=False) session_entry = models.Session(session_name=session_name, active=False)
db.add(session_entry) db.add(session_entry)
db.commit() db.commit()
def get_setting(db, key):
entry = db.query(models.Config.value).filter(models.Config.key == key).first()
if entry:
return entry[0]
else:
return None
def set_setting(db, key, value):
setting_entry = db.query(models.Config).filter(models.Config.key == key).first()
if setting_entry:
setting_entry.value = value
else:
setting_entry = models.Config(key=key, value=value)
db.add(setting_entry)
db.commit()

View File

@@ -21,5 +21,6 @@ async def get_db():
class Base(DeclarativeBase): class Base(DeclarativeBase):
type_annotation_map = { type_annotation_map = {
dict[str, bool]: PickleType dict[str, bool]: PickleType,
object: PickleType
} }

View File

@@ -4,10 +4,11 @@ from fastapi.staticfiles import StaticFiles
from fastapi.responses import HTMLResponse from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from app.database import engine, Base, get_db from app.database import engine, Base, get_db
from app.crud import get_songs_and_vote_for_session from app.crud import get_songs_and_vote_for_session, get_setting
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from typing import Annotated from typing import Annotated
from app.schemas import Song from app.schemas import Song
import json
Base.metadata.create_all(engine) Base.metadata.create_all(engine)
@@ -31,17 +32,23 @@ async def root(request: Request) -> HTMLResponse:
@app.get("/vote") @app.get("/vote")
async def vote(request: Request, session_id: str, db: Annotated[Session, Depends(get_db)]) -> HTMLResponse: async def vote(request: Request, session_id: str, db: Session = Depends(get_db)) -> HTMLResponse:
songs = [Song(**s.__dict__, vote=v) veto_mode = get_setting(db, "veto_mode")
for s, v in get_songs_and_vote_for_session(db, session_id)]
songs = [Song(**s.__dict__, vote=v, vote_comment=c)
for s, v, c in get_songs_and_vote_for_session(db, session_id)]
songs_by_category = {} songs_by_category = {}
all_categories = set() all_categories = set()
wildcard_songs = [] wildcard_songs = []
current_songs = [] current_songs = []
other_songs = []
for song in songs: for song in songs:
if (not song.singable) and (not veto_mode):
continue
if song.is_current: if song.is_current:
current_songs.append(song) current_songs.append(song)
continue continue
@@ -50,26 +57,35 @@ async def vote(request: Request, session_id: str, db: Annotated[Session, Depends
wildcard_songs.append(song) wildcard_songs.append(song)
continue continue
if not song.main_category:
other_songs.append(song)
continue
if song.main_category not in songs_by_category: if song.main_category not in songs_by_category:
songs_by_category[song.main_category] = [] songs_by_category[song.main_category] = []
songs_by_category[song.main_category].append(song) songs_by_category[song.main_category].append(song)
all_categories.update(song.categories.keys()) all_categories.update(song.categories.keys())
songs_by_category["Sonstige"] = other_songs
songs_by_category["Wildcard (nicht a cappella)"] = wildcard_songs songs_by_category["Wildcard (nicht a cappella)"] = wildcard_songs
songs_by_category["Aktuelles Programm"] = current_songs songs_by_category["Aktuelles Programm"] = current_songs
all_categories = list(all_categories) all_categories = list(all_categories)
all_categories.sort() all_categories.sort()
all_categories.append("Sonstige")
all_categories.append("Wildcard (nicht a cappella)") all_categories.append("Wildcard (nicht a cappella)")
all_categories.append("Aktuelles Programm") all_categories.append("Aktuelles Programm")
print(all_categories) # print(all_categories)
# with open('/data/songs_by_cat.json', 'w') as f:
# json.dump({cat : [s.__dict__ for s in songs] for cat, songs in songs_by_category.items()}, f)
return templates.TemplateResponse( return templates.TemplateResponse(
request=request, name="voting.html", context={ request=request, name="voting.html", context={
"songs_by_category": songs_by_category, "songs_by_category": songs_by_category,
"all_categories": {c: i+1 for i, c in enumerate(all_categories)}, "all_categories": {c: i+1 for i, c in enumerate(all_categories)},
"session_id": session_id "session_id": session_id,
"veto_mode": veto_mode
} }
) )

View File

@@ -24,6 +24,7 @@ class Song(Base):
categories: Mapped[Optional[dict[str, bool]]] categories: Mapped[Optional[dict[str, bool]]]
main_category: Mapped[Optional[str]] main_category: Mapped[Optional[str]]
singable: Mapped[Optional[bool]] singable: Mapped[Optional[bool]]
comment: Mapped[Optional[str]]
class Session(Base): class Session(Base):
@@ -42,6 +43,13 @@ class Vote(Base):
song_id: Mapped[int] = mapped_column(Integer, ForeignKey("songs.id")) song_id: Mapped[int] = mapped_column(Integer, ForeignKey("songs.id"))
session_id: Mapped[int] = mapped_column(Integer, ForeignKey("sessions.id")) session_id: Mapped[int] = mapped_column(Integer, ForeignKey("sessions.id"))
vote: Mapped[Optional[int]] vote: Mapped[Optional[int]]
comment: Mapped[Optional[str]]
time_created: Mapped[datetime] = mapped_column(server_default=func.now()) time_created: Mapped[datetime] = mapped_column(server_default=func.now())
time_updated: Mapped[Optional[datetime] time_updated: Mapped[Optional[datetime]
] = mapped_column(onupdate=func.now()) ] = mapped_column(onupdate=func.now())
class Config(Base):
__tablename__ = 'config'
#id: Mapped[int] = mapped_column(primary_key=True)
key: Mapped[str] = mapped_column(primary_key=True)
value: Mapped[object]

View File

@@ -8,7 +8,7 @@ from sqlalchemy.orm import Session
from app.database import get_db, engine, Base from app.database import get_db, engine, Base
from app.routers.user import get_current_user from app.routers.user import get_current_user
from app.crud import create_song from app.crud import create_song, get_setting, set_setting
router = APIRouter( router = APIRouter(
prefix="/admin", prefix="/admin",
@@ -66,7 +66,7 @@ def get_spotify_id(url):
@router.post("/load_list") @router.post("/load_list")
async def create_upload_file(db: Session = Depends(get_db)): async def create_upload_file(include_non_singable: bool = False, db: Session = Depends(get_db)):
Base.metadata.drop_all(engine) Base.metadata.drop_all(engine)
Base.metadata.create_all(engine) Base.metadata.create_all(engine)
@@ -78,16 +78,24 @@ async def create_upload_file(db: Session = Depends(get_db)):
category_names = list(song_list.iloc[0][8:17]) category_names = list(song_list.iloc[0][8:17])
for i, row in song_list[1:].iterrows(): for i, row in song_list[1:].iterrows():
if (row[17] == "nein") and not include_non_singable:
continue
row = np.array(row) row = np.array(row)
if not row[2]: # no title
continue
yt_id = get_youtube_id(row[3]) yt_id = get_youtube_id(row[3])
spfy_id = get_spotify_id(row[3]) spfy_id = get_spotify_id(row[3])
categories = {n: v for n, v in zip( categories = {n: v for n, v in zip(
category_names, row[8:17] != None)} category_names, row[8:17] != None)}
if (not np.any(list(categories.values()))) and (row[5] != "ja"): if (not np.any(list(categories.values()))):
continue main_category = None
else:
main_category = category_names[get_main_category(row[8:17])]
create_song(db, create_song(db,
og_artist=row[0], og_artist=row[0],
@@ -102,6 +110,18 @@ async def create_upload_file(db: Session = Depends(get_db)):
is_aca=row[6] == "ja", is_aca=row[6] == "ja",
arng_url=row[7], arng_url=row[7],
categories=categories, categories=categories,
main_category=category_names[get_main_category(row[8:17])], main_category=main_category,
singable=row[17] != "nein" singable=row[17] != "nein",
comment=row[18]
) )
@router.post("/toggle_veto_mode")
async def toggle_veto_mode(db: Session = Depends(get_db)) -> bool:
veto_setting = get_setting(db, "veto_mode")
if veto_setting:
set_setting(db, "veto_mode", False)
return False
else:
set_setting(db, "veto_mode", True)
return True

View File

@@ -5,7 +5,7 @@ from sqlalchemy.orm import Session
import app.models as models import app.models as models
from app.database import get_db from app.database import get_db
from app.schemas import Song from app.schemas import Song
from app.crud import get_songs_and_vote_for_session, create_or_update_vote, get_all_songs_and_votes from app.crud import get_songs_and_vote_for_session, create_or_update_vote, get_all_songs_and_votes, create_or_update_comment
router = APIRouter( router = APIRouter(
prefix="/songs", prefix="/songs",
@@ -16,13 +16,17 @@ router = APIRouter(
@router.get("/") @router.get("/")
async def get_songs(session_id: str = "", db: Annotated[Session, Depends(get_db)] = None) -> list[Song]: async def get_songs(session_id: str = "", db: Annotated[Session, Depends(get_db)] = None) -> list[Song]:
return [Song(**s.__dict__, vote=v) for s, v in get_songs_and_vote_for_session(db, session_id)] return [Song(**s.__dict__, vote=v, vote_comment=c) for s, v, c in get_songs_and_vote_for_session(db, session_id)]
@router.post("/{song_id}/vote") @router.post("/{song_id}/vote")
async def vote(song_id: str, session_id: str, vote: int, db: Annotated[Session, Depends(get_db)]): async def vote(song_id: str, session_id: str, vote: int, db: Annotated[Session, Depends(get_db)]):
create_or_update_vote(db, song_id, session_id, vote) create_or_update_vote(db, song_id, session_id, vote)
@router.post("/{song_id}/comment")
async def comment(song_id: str, session_id: str, comment: str, db: Annotated[Session, Depends(get_db)]):
create_or_update_comment(db, song_id, session_id, comment)
#create_or_update_vote(db, song_id, session_id, vote)
@router.get("/evaluation") @router.get("/evaluation")
async def get_evaluation(db: Annotated[Session, Depends(get_db)] = None) -> dict[int, dict[int, int]]: async def get_evaluation(db: Annotated[Session, Depends(get_db)] = None) -> dict[int, dict[int, int]]:

View File

@@ -17,4 +17,6 @@ class Song(BaseModel):
categories: Optional[dict[str, bool]] categories: Optional[dict[str, bool]]
main_category: Optional[str] main_category: Optional[str]
singable: Optional[bool] singable: Optional[bool]
comment: Optional[str]
vote: Optional[int] vote: Optional[int]
vote_comment: Optional[str]

View File

@@ -224,8 +224,6 @@
font-size: 0.7em; font-size: 0.7em;
} }
h1 { h1 {
font-family: sans-serif; font-family: sans-serif;
padding: 0.1em; padding: 0.1em;

View File

@@ -9,6 +9,19 @@
<link rel="shortcut icon" href="https://choriosity.de/assets/images/favicon.svg" type="image/svg+xml"> <link rel="shortcut icon" href="https://choriosity.de/assets/images/favicon.svg" type="image/svg+xml">
<link rel="stylesheet" type="text/css" href="/static/site.css"> <link rel="stylesheet" type="text/css" href="/static/site.css">
{% if veto_mode %}
<style type="text/css">
.comment {
width: 100%;
margin-top: 0.3em;
font-size: 1.2em;
}
.not_singable {
background-color: color-mix(in srgb, #e1412f 30%, #f0f0f0);
}
</style>
{% endif %}
<script src="https://open.spotify.com/embed/iframe-api/v1" async></script> <script src="https://open.spotify.com/embed/iframe-api/v1" async></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script> <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<script type="text/javascript"> <script type="text/javascript">
@@ -119,12 +132,22 @@
switch (vote) { switch (vote) {
case 0: case 0:
neutral_button.addClass("selected") neutral_button.addClass("selected")
{% if veto_mode %}
$("#song-" + song_id).removeClass("not_singable")
{% endif %}
break; break;
case 1: case 1:
yes_button.addClass("selected") yes_button.addClass("selected")
{% if veto_mode %}
$("#song-" + song_id).removeClass("not_singable")
{% endif %}
break; break;
case -1: case -1:
no_button.addClass("selected") no_button.addClass("selected")
{% if veto_mode %}
$("#song-" + song_id).addClass("not_singable")
{% endif %}
break;
default: default:
break; break;
} }
@@ -134,18 +157,35 @@
method: "POST" method: "POST"
}) })
} }
{% if veto_mode %}
function updateComment(song_id, el) {
comment = el.value
$.ajax({
url: "/songs/" + song_id + "/comment?" + $.param({ session_id: session_id, comment: comment }),
method: "POST"
})
}
{% endif %}
</script> </script>
</head> </head>
<body> <body>
{% if veto_mode %}
<h1>Veto Mode</h1>
<div class="text">Du kannst ungeeignete Vorschläge durch eine Nein-Stimme markieren und Kommentare zu allen Liedern abgeben.
</div>
{% else %}
<h1>Hallo :)</h1> <h1>Hallo :)</h1>
<div class="text">Du kannst die Liederwahl jederzeit unterbrechen und zu einem späteren Zeitpunkt weitermachen. <div class="text">Du kannst die Liederwahl jederzeit unterbrechen und zu einem späteren Zeitpunkt weitermachen.
</div> </div>
{% endif %}
<div id="songs"> <div id="songs">
{% for main_category, songs in songs_by_category.items() %} {% for main_category, songs in songs_by_category.items() %}
<h1 style="--hue: {{ all_categories[main_category] / all_categories|length }};">{{ main_category }}</h1> <h1 style="--hue: {{ all_categories[main_category] / all_categories|length }};">{{ main_category }}</h1>
{% for song in songs -%} {% for song in songs -%}
<div class="song" id="song-{{ song.id }}"> <div class="song{% if (song.vote == -1) or (not song.vote and not song.singable) %} not_singable{% endif %}"
id="song-{{ song.id }}">
<div class="cover-container"> <div class="cover-container">
<img src="{{ song.thumbnail }}" class="cover"> <img src="{{ song.thumbnail }}" class="cover">
<div class="overlay" <div class="overlay"
@@ -163,7 +203,7 @@
endif %}{% endfor %}<span style="--main-color: transparent;">&nbsp;</span> endif %}{% endfor %}<span style="--main-color: transparent;">&nbsp;</span>
</div> </div>
<div class="vote-buttons"> <div class="vote-buttons">
<div class="button button-no {% if song.vote == -1 %}selected{% endif %}" <div class="button button-no {% if (song.vote == -1) or (not song.vote and not song.singable) %}selected{% endif %}"
onmousedown="vote({{ song.id }}, -1); return false;" onclick="return false;"><img onmousedown="vote({{ song.id }}, -1); return false;" onclick="return false;"><img
src="/static/no.svg"> src="/static/no.svg">
</div> </div>
@@ -176,6 +216,11 @@
</div> </div>
</div> </div>
<div class="clear"></div> <div class="clear"></div>
{% if veto_mode %}
<input type="text" class="comment"
value="{% if song.vote_comment %}{{ song.vote_comment }}{% else %}{% if song.comment %}{{ song.comment }}{% else %}{% endif %}{% endif %}"
placeholder="Kommentar" onchange="updateComment({{ song.id }}, this);">
{% endif %}
</div> </div>
{% endfor %} {% endfor %}
{% endfor %} {% endfor %}