v1.0
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
FROM python:3.12-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 5000
|
||||
|
||||
CMD ["python", "run.py"]
|
||||
@@ -3,6 +3,7 @@ from flask import Flask
|
||||
from flask_migrate import Migrate
|
||||
from flask_socketio import SocketIO
|
||||
from dotenv import load_dotenv
|
||||
from sqlalchemy import text
|
||||
from .models import db
|
||||
|
||||
socketio = SocketIO(cors_allowed_origins="*")
|
||||
@@ -20,6 +21,14 @@ def create_app():
|
||||
)
|
||||
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
|
||||
|
||||
@app.after_request
|
||||
def add_cors_headers(response):
|
||||
origin = os.getenv("FRONTEND_ORIGIN", "*")
|
||||
response.headers["Access-Control-Allow-Origin"] = origin
|
||||
response.headers["Access-Control-Allow-Headers"] = "Content-Type"
|
||||
response.headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS"
|
||||
return response
|
||||
|
||||
db.init_app(app)
|
||||
migrate.init_app(app, db)
|
||||
socketio.init_app(app, cors_allowed_origins=os.getenv("FRONTEND_ORIGIN", "*"))
|
||||
@@ -29,4 +38,24 @@ def create_app():
|
||||
|
||||
from .socket import events # noqa: F401
|
||||
|
||||
with app.app_context():
|
||||
ensure_schema()
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def ensure_schema():
|
||||
db.create_all()
|
||||
engine_name = db.engine.url.get_backend_name()
|
||||
if engine_name != "postgresql":
|
||||
return
|
||||
|
||||
db.session.execute(text(
|
||||
"ALTER TABLE session_settings "
|
||||
"ADD COLUMN IF NOT EXISTS match_mode VARCHAR(20) NOT NULL DEFAULT 'first_match'"
|
||||
))
|
||||
db.session.execute(text(
|
||||
"ALTER TABLE session_settings "
|
||||
"ADD COLUMN IF NOT EXISTS voting_round INTEGER NOT NULL DEFAULT 1"
|
||||
))
|
||||
db.session.commit()
|
||||
|
||||
@@ -2,4 +2,4 @@ from flask_sqlalchemy import SQLAlchemy
|
||||
|
||||
db = SQLAlchemy()
|
||||
|
||||
from .session import Session, Participant, Recipe, Vote # noqa: E402,F401
|
||||
from .session import Session, SessionSettings, Participant, Recipe, Vote # noqa: E402,F401
|
||||
|
||||
@@ -13,7 +13,14 @@ class Session(db.Model):
|
||||
created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
|
||||
|
||||
participants = db.relationship("Participant", backref="session", cascade="all, delete-orphan")
|
||||
recipes = db.relationship("Recipe", backref="session", cascade="all, delete-orphan", order_by="Recipe.position")
|
||||
settings = db.relationship("SessionSettings", backref="session", cascade="all, delete-orphan", uselist=False)
|
||||
recipes = db.relationship(
|
||||
"Recipe",
|
||||
backref="session",
|
||||
cascade="all, delete-orphan",
|
||||
foreign_keys="Recipe.session_id",
|
||||
order_by="Recipe.position",
|
||||
)
|
||||
|
||||
|
||||
class Participant(db.Model):
|
||||
@@ -26,6 +33,16 @@ class Participant(db.Model):
|
||||
created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
|
||||
|
||||
|
||||
class SessionSettings(db.Model):
|
||||
__tablename__ = "session_settings"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
session_id = db.Column(db.Integer, db.ForeignKey("sessions.id"), nullable=False, unique=True)
|
||||
required_likes = db.Column(db.Integer, nullable=False, default=2)
|
||||
match_mode = db.Column(db.String(20), nullable=False, default="first_match")
|
||||
voting_round = db.Column(db.Integer, nullable=False, default=1)
|
||||
|
||||
|
||||
class Recipe(db.Model):
|
||||
__tablename__ = "recipes"
|
||||
|
||||
|
||||
@@ -2,8 +2,9 @@ import random
|
||||
import string
|
||||
from flask import Blueprint, jsonify, request
|
||||
from ..models import db
|
||||
from ..models.session import Session, Participant, Recipe
|
||||
from ..services.recipes import get_recipes
|
||||
from ..models.session import Session, Participant
|
||||
from ..services.session_recipes import add_recipe_batch, serialize_recipe
|
||||
from ..services.session_settings import get_match_mode, get_required_likes, get_voting_round
|
||||
|
||||
bp = Blueprint("sessions", __name__)
|
||||
|
||||
@@ -22,18 +23,11 @@ def serialize_session(session):
|
||||
{"id": p.id, "name": p.name, "isHost": p.is_host}
|
||||
for p in session.participants
|
||||
],
|
||||
"recipes": [
|
||||
{
|
||||
"id": r.id,
|
||||
"title": r.title,
|
||||
"url": r.url,
|
||||
"imageUrl": r.image_url,
|
||||
"meta": r.meta,
|
||||
"position": r.position,
|
||||
}
|
||||
for r in session.recipes
|
||||
],
|
||||
"recipes": [serialize_recipe(recipe) for recipe in session.recipes],
|
||||
"matchedRecipeId": session.matched_recipe_id,
|
||||
"requiredLikes": get_required_likes(session),
|
||||
"matchMode": get_match_mode(session),
|
||||
"votingRound": get_voting_round(session),
|
||||
}
|
||||
|
||||
|
||||
@@ -53,8 +47,7 @@ def create_session():
|
||||
host = Participant(session_id=session.id, name=name, is_host=True)
|
||||
db.session.add(host)
|
||||
|
||||
for index, recipe_data in enumerate(get_recipes()):
|
||||
db.session.add(Recipe(session_id=session.id, position=index, **recipe_data))
|
||||
add_recipe_batch(session)
|
||||
|
||||
db.session.commit()
|
||||
return jsonify({"session": serialize_session(session), "participantId": host.id})
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
from ..models import db
|
||||
from ..models.session import Vote
|
||||
from .session_recipes import add_recipe_batch
|
||||
from .session_settings import FINAL_ROUND_MODE, get_match_mode, get_required_likes, get_voting_round, set_voting_round
|
||||
|
||||
|
||||
def get_qualified_recipes(session):
|
||||
required_likes = get_required_likes(session)
|
||||
qualified = []
|
||||
|
||||
for recipe in session.recipes:
|
||||
yes_count = Vote.query.filter_by(
|
||||
session_id=session.id,
|
||||
recipe_id=recipe.id,
|
||||
vote="yes",
|
||||
).count()
|
||||
if yes_count >= required_likes:
|
||||
qualified.append(recipe)
|
||||
|
||||
return qualified
|
||||
|
||||
|
||||
def all_participants_finished_current_recipes(session):
|
||||
recipe_count = len(session.recipes)
|
||||
if recipe_count == 0:
|
||||
return False
|
||||
|
||||
for participant in session.participants:
|
||||
vote_count = Vote.query.filter_by(
|
||||
session_id=session.id,
|
||||
participant_id=participant.id,
|
||||
).count()
|
||||
if vote_count < recipe_count:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def enter_final_round(session, qualified_recipes):
|
||||
qualified_ids = {recipe.id for recipe in qualified_recipes}
|
||||
|
||||
session.matched_recipe_id = None
|
||||
Vote.query.filter_by(session_id=session.id).delete()
|
||||
|
||||
for recipe in list(session.recipes):
|
||||
if recipe.id not in qualified_ids:
|
||||
session.recipes.remove(recipe)
|
||||
db.session.delete(recipe)
|
||||
|
||||
db.session.flush()
|
||||
|
||||
finalist_recipes = sorted(session.recipes, key=lambda recipe: recipe.position)
|
||||
for index, recipe in enumerate(finalist_recipes):
|
||||
recipe.position = index
|
||||
|
||||
set_voting_round(session, 2)
|
||||
|
||||
|
||||
def handle_final_round_vote(session, recipe):
|
||||
yes_count = Vote.query.filter_by(session_id=session.id, recipe_id=recipe.id, vote="yes").count()
|
||||
if get_voting_round(session) == 2 and yes_count >= get_required_likes(session):
|
||||
session.status = "finished"
|
||||
session.matched_recipe_id = recipe.id
|
||||
return "match"
|
||||
|
||||
if all_participants_finished_current_recipes(session):
|
||||
qualified_recipes = get_qualified_recipes(session)
|
||||
if get_voting_round(session) == 1:
|
||||
if len(qualified_recipes) >= 3:
|
||||
enter_final_round(session, qualified_recipes)
|
||||
return "final_round"
|
||||
|
||||
add_recipe_batch(session)
|
||||
return "more_recipes"
|
||||
|
||||
if qualified_recipes:
|
||||
session.status = "finished"
|
||||
session.matched_recipe_id = qualified_recipes[0].id
|
||||
return "match"
|
||||
|
||||
set_voting_round(session, 1)
|
||||
add_recipe_batch(session)
|
||||
return "more_recipes"
|
||||
|
||||
return "update"
|
||||
|
||||
|
||||
def is_final_round_mode(session):
|
||||
return get_match_mode(session) == FINAL_ROUND_MODE
|
||||
@@ -1,4 +1,14 @@
|
||||
DUMMY_RECIPES = [
|
||||
import random
|
||||
import re
|
||||
from html import unescape
|
||||
from html.parser import HTMLParser
|
||||
from urllib.parse import urljoin
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
|
||||
CHEFKOCH_TODAY_URL = "https://www.chefkoch.de/rezepte/was-koche-ich-heute/"
|
||||
|
||||
FALLBACK_RECIPES = [
|
||||
{
|
||||
"title": "Cremige Pasta mit Tomaten",
|
||||
"url": "https://www.chefkoch.de/",
|
||||
@@ -50,6 +60,90 @@ DUMMY_RECIPES = [
|
||||
]
|
||||
|
||||
|
||||
class ChefkochTodayParser(HTMLParser):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.recipes = []
|
||||
self._current_link = None
|
||||
self._current_text = []
|
||||
self._current_image_url = ""
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
attrs = dict(attrs)
|
||||
if tag == "a":
|
||||
href = attrs.get("href", "")
|
||||
if re.search(r"/rezepte/\d+/.+\.html", href):
|
||||
self._current_link = urljoin(CHEFKOCH_TODAY_URL, href)
|
||||
self._current_text = []
|
||||
self._current_image_url = ""
|
||||
elif tag == "img" and self._current_link:
|
||||
self._current_image_url = attrs.get("src") or attrs.get("data-src") or ""
|
||||
|
||||
def handle_data(self, data):
|
||||
if self._current_link:
|
||||
self._current_text.append(data)
|
||||
|
||||
def handle_endtag(self, tag):
|
||||
if tag != "a" or not self._current_link:
|
||||
return
|
||||
|
||||
title, meta = parse_recipe_text(" ".join(self._current_text))
|
||||
if title and not any(recipe["url"] == self._current_link for recipe in self.recipes):
|
||||
self.recipes.append(
|
||||
{
|
||||
"title": title,
|
||||
"url": self._current_link,
|
||||
"image_url": self._current_image_url,
|
||||
"meta": meta,
|
||||
}
|
||||
)
|
||||
|
||||
self._current_link = None
|
||||
self._current_text = []
|
||||
self._current_image_url = ""
|
||||
|
||||
|
||||
def parse_recipe_text(text):
|
||||
text = unescape(re.sub(r"\s+", " ", text)).strip()
|
||||
text = text.replace("rating_star", "").strip()
|
||||
|
||||
rating_match = re.match(r"(?P<rating>\d+[,.]\d)\s*/?\s*5?\s*\((?P<count>\d+)\)\s*(?P<title>.+)", text)
|
||||
if rating_match:
|
||||
rating = rating_match.group("rating")
|
||||
count = rating_match.group("count")
|
||||
title = clean_recipe_title(rating_match.group("title"))
|
||||
return title, f"{rating}/5 ({count})"
|
||||
|
||||
return clean_recipe_title(text), ""
|
||||
|
||||
|
||||
def clean_recipe_title(title):
|
||||
title = re.sub(r"\s+von\s+essen\s*&\s*trinken.*$", "", title, flags=re.IGNORECASE)
|
||||
title = re.sub(r"[^\w\sÄÖÜäöüß.,&'’:/()\-]", "", title)
|
||||
return re.sub(r"\s+", " ", title).strip()
|
||||
|
||||
|
||||
def fetch_chefkoch_today_recipes():
|
||||
cache_buster = random.randint(100000, 999999)
|
||||
request = Request(
|
||||
f"{CHEFKOCH_TODAY_URL}?rf={cache_buster}",
|
||||
headers={
|
||||
"User-Agent": "Mozilla/5.0 (compatible; RezeptFinder/1.0)",
|
||||
"Cache-Control": "no-cache",
|
||||
},
|
||||
)
|
||||
with urlopen(request, timeout=8) as response:
|
||||
html = response.read().decode("utf-8", errors="replace")
|
||||
|
||||
parser = ChefkochTodayParser()
|
||||
parser.feed(html)
|
||||
return parser.recipes[:8]
|
||||
|
||||
|
||||
def get_recipes():
|
||||
# Später ersetzen durch Chefkoch-Vorschau-Importer.
|
||||
return DUMMY_RECIPES
|
||||
try:
|
||||
recipes = fetch_chefkoch_today_recipes()
|
||||
except Exception:
|
||||
return FALLBACK_RECIPES
|
||||
|
||||
return recipes or FALLBACK_RECIPES
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
from ..models import db
|
||||
from ..models.session import Recipe, Vote
|
||||
from .recipes import get_recipes
|
||||
|
||||
|
||||
def serialize_recipe(recipe):
|
||||
return {
|
||||
"id": recipe.id,
|
||||
"title": recipe.title,
|
||||
"url": recipe.url,
|
||||
"imageUrl": recipe.image_url,
|
||||
"meta": recipe.meta,
|
||||
"position": recipe.position,
|
||||
}
|
||||
|
||||
|
||||
def add_recipe_batch(session, size=8):
|
||||
existing_urls = {recipe.url for recipe in session.recipes}
|
||||
next_position = len(session.recipes)
|
||||
added = 0
|
||||
|
||||
for _ in range(3):
|
||||
for recipe_data in get_recipes():
|
||||
if recipe_data["url"] in existing_urls:
|
||||
continue
|
||||
|
||||
session.recipes.append(Recipe(position=next_position, **recipe_data))
|
||||
existing_urls.add(recipe_data["url"])
|
||||
next_position += 1
|
||||
added += 1
|
||||
|
||||
if added == size:
|
||||
db.session.flush()
|
||||
return added
|
||||
|
||||
if added:
|
||||
db.session.flush()
|
||||
return added
|
||||
|
||||
for recipe_data in get_recipes()[:size]:
|
||||
session.recipes.append(Recipe(position=next_position, **recipe_data))
|
||||
next_position += 1
|
||||
added += 1
|
||||
|
||||
db.session.flush()
|
||||
return added
|
||||
|
||||
|
||||
def replace_recipe_batch(session, size=8):
|
||||
session.matched_recipe_id = None
|
||||
Vote.query.filter_by(session_id=session.id).delete()
|
||||
for recipe in list(session.recipes):
|
||||
db.session.delete(recipe)
|
||||
|
||||
db.session.flush()
|
||||
session.recipes.clear()
|
||||
return add_recipe_batch(session, size)
|
||||
@@ -0,0 +1,62 @@
|
||||
from ..models import db
|
||||
from ..models.session import SessionSettings
|
||||
|
||||
FIRST_MATCH_MODE = "first_match"
|
||||
FINAL_ROUND_MODE = "final_round"
|
||||
MATCH_MODES = {FIRST_MATCH_MODE, FINAL_ROUND_MODE}
|
||||
|
||||
|
||||
def get_required_likes(session):
|
||||
participant_count = max(2, len(session.participants))
|
||||
if not session.settings:
|
||||
return participant_count
|
||||
|
||||
return max(2, min(session.settings.required_likes, participant_count))
|
||||
|
||||
|
||||
def set_required_likes(session, required_likes):
|
||||
participant_count = max(2, len(session.participants))
|
||||
required_likes = max(2, min(int(required_likes), participant_count))
|
||||
|
||||
if not session.settings:
|
||||
session.settings = SessionSettings(required_likes=required_likes)
|
||||
db.session.add(session.settings)
|
||||
else:
|
||||
session.settings.required_likes = required_likes
|
||||
|
||||
return required_likes
|
||||
|
||||
|
||||
def get_match_mode(session):
|
||||
if not session.settings:
|
||||
return FIRST_MATCH_MODE
|
||||
|
||||
return session.settings.match_mode if session.settings.match_mode in MATCH_MODES else FIRST_MATCH_MODE
|
||||
|
||||
|
||||
def get_voting_round(session):
|
||||
if not session.settings:
|
||||
return 1
|
||||
|
||||
return max(1, session.settings.voting_round)
|
||||
|
||||
|
||||
def ensure_settings(session):
|
||||
if not session.settings:
|
||||
session.settings = SessionSettings()
|
||||
db.session.add(session.settings)
|
||||
|
||||
return session.settings
|
||||
|
||||
|
||||
def set_match_mode(session, match_mode):
|
||||
settings = ensure_settings(session)
|
||||
settings.match_mode = match_mode if match_mode in MATCH_MODES else FIRST_MATCH_MODE
|
||||
settings.voting_round = 1
|
||||
return settings.match_mode
|
||||
|
||||
|
||||
def set_voting_round(session, voting_round):
|
||||
settings = ensure_settings(session)
|
||||
settings.voting_round = max(1, int(voting_round))
|
||||
return settings.voting_round
|
||||
@@ -3,23 +3,15 @@ from sqlalchemy.exc import IntegrityError
|
||||
from .. import socketio
|
||||
from ..models import db
|
||||
from ..models.session import Session, Vote
|
||||
from ..services.match_modes import handle_final_round_vote, is_final_round_mode
|
||||
from ..services.session_recipes import add_recipe_batch, replace_recipe_batch, serialize_recipe
|
||||
from ..services.session_settings import get_match_mode, get_required_likes, get_voting_round, set_match_mode, set_required_likes, set_voting_round
|
||||
|
||||
|
||||
def room_name(code):
|
||||
return f"session:{code.upper()}"
|
||||
|
||||
|
||||
def serialize_recipe(recipe):
|
||||
return {
|
||||
"id": recipe.id,
|
||||
"title": recipe.title,
|
||||
"url": recipe.url,
|
||||
"imageUrl": recipe.image_url,
|
||||
"meta": recipe.meta,
|
||||
"position": recipe.position,
|
||||
}
|
||||
|
||||
|
||||
def emit_session_update(session):
|
||||
socketio.emit(
|
||||
"session:update",
|
||||
@@ -32,6 +24,10 @@ def emit_session_update(session):
|
||||
{"id": p.id, "name": p.name, "isHost": p.is_host}
|
||||
for p in session.participants
|
||||
],
|
||||
"recipes": [serialize_recipe(recipe) for recipe in session.recipes],
|
||||
"requiredLikes": get_required_likes(session),
|
||||
"matchMode": get_match_mode(session),
|
||||
"votingRound": get_voting_round(session),
|
||||
},
|
||||
room=room_name(session.code),
|
||||
)
|
||||
@@ -65,6 +61,76 @@ def start_session(data):
|
||||
|
||||
session.status = "running"
|
||||
session.current_recipe_index = 0
|
||||
session.matched_recipe_id = None
|
||||
set_voting_round(session, 1)
|
||||
db.session.commit()
|
||||
emit_session_update(session)
|
||||
|
||||
|
||||
@socketio.on("session:set_required_likes")
|
||||
def update_required_likes(data):
|
||||
code = data.get("code", "").upper()
|
||||
participant_id = data.get("participantId")
|
||||
session = Session.query.filter_by(code=code).first()
|
||||
if not session:
|
||||
emit("error", {"message": "Session nicht gefunden."})
|
||||
return
|
||||
|
||||
host = next((p for p in session.participants if p.id == participant_id and p.is_host), None)
|
||||
if not host:
|
||||
emit("error", {"message": "Nur der Host kann die Match-Regel ändern."})
|
||||
return
|
||||
|
||||
if session.status != "waiting":
|
||||
emit("error", {"message": "Die Match-Regel kann nur in der Lobby geändert werden."})
|
||||
return
|
||||
|
||||
set_required_likes(session, data.get("requiredLikes", len(session.participants)))
|
||||
db.session.commit()
|
||||
emit_session_update(session)
|
||||
|
||||
|
||||
@socketio.on("session:set_match_mode")
|
||||
def update_match_mode(data):
|
||||
code = data.get("code", "").upper()
|
||||
participant_id = data.get("participantId")
|
||||
session = Session.query.filter_by(code=code).first()
|
||||
if not session:
|
||||
emit("error", {"message": "Session nicht gefunden."})
|
||||
return
|
||||
|
||||
host = next((p for p in session.participants if p.id == participant_id and p.is_host), None)
|
||||
if not host:
|
||||
emit("error", {"message": "Nur der Host kann den Modus ändern."})
|
||||
return
|
||||
|
||||
if session.status != "waiting":
|
||||
emit("error", {"message": "Der Modus kann nur in der Lobby geändert werden."})
|
||||
return
|
||||
|
||||
set_match_mode(session, data.get("matchMode"))
|
||||
db.session.commit()
|
||||
emit_session_update(session)
|
||||
|
||||
|
||||
@socketio.on("session:restart")
|
||||
def restart_session(data):
|
||||
code = data.get("code", "").upper()
|
||||
participant_id = data.get("participantId")
|
||||
session = Session.query.filter_by(code=code).first()
|
||||
if not session:
|
||||
emit("error", {"message": "Session nicht gefunden."})
|
||||
return
|
||||
|
||||
host = next((p for p in session.participants if p.id == participant_id and p.is_host), None)
|
||||
if not host:
|
||||
emit("error", {"message": "Nur der Host kann neu starten."})
|
||||
return
|
||||
|
||||
session.status = "running"
|
||||
session.current_recipe_index = 0
|
||||
set_voting_round(session, 1)
|
||||
replace_recipe_batch(session)
|
||||
db.session.commit()
|
||||
emit_session_update(session)
|
||||
|
||||
@@ -97,11 +163,17 @@ def vote(data):
|
||||
db.session.rollback()
|
||||
return
|
||||
|
||||
participant_count = len(session.participants)
|
||||
yes_count = Vote.query.filter_by(session_id=session.id, recipe_id=recipe.id, vote="yes").count()
|
||||
vote_count = Vote.query.filter_by(session_id=session.id, recipe_id=recipe.id).count()
|
||||
if is_final_round_mode(session):
|
||||
result = handle_final_round_vote(session, recipe)
|
||||
db.session.commit()
|
||||
if result == "match":
|
||||
matched_recipe = next((r for r in session.recipes if r.id == session.matched_recipe_id), recipe)
|
||||
socketio.emit("session:match", {"recipe": serialize_recipe(matched_recipe)}, room=room_name(code))
|
||||
emit_session_update(session)
|
||||
return
|
||||
|
||||
if yes_count == participant_count:
|
||||
yes_count = Vote.query.filter_by(session_id=session.id, recipe_id=recipe.id, vote="yes").count()
|
||||
if yes_count >= get_required_likes(session):
|
||||
session.status = "finished"
|
||||
session.matched_recipe_id = recipe.id
|
||||
db.session.commit()
|
||||
@@ -109,9 +181,12 @@ def vote(data):
|
||||
emit_session_update(session)
|
||||
return
|
||||
|
||||
if vote_count == participant_count:
|
||||
session.current_recipe_index += 1
|
||||
if session.current_recipe_index >= len(session.recipes):
|
||||
session.status = "finished"
|
||||
db.session.commit()
|
||||
emit_session_update(session)
|
||||
participant_vote_count = Vote.query.filter_by(
|
||||
session_id=session.id,
|
||||
participant_id=participant_id,
|
||||
).count()
|
||||
if participant_vote_count >= len(session.recipes):
|
||||
add_recipe_batch(session)
|
||||
|
||||
db.session.commit()
|
||||
emit_session_update(session)
|
||||
|
||||
Reference in New Issue
Block a user