Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate front-end to Vue.js #15

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Remember to keep in sync with the frontend's .env too
CATEGORIES_PER_GAME=5
QUESTIONS_PER_CATEGORY=5
SCORE_TICK=100
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
lts/*
11 changes: 11 additions & 0 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,14 @@ database where transactions are pushed when the hosts submit the points. This
has the flipside requiring games to be finalized before a new one can be
started. Make sure that you always push the "Game over" button before
reloading to start a new game.

== Dev

Use appropriate node version. A `.nvmrc` file exists to ease setup.
Once you have https://github.com/nvm-sh/nvm#nvmrc[installed nvm adequately] (or https://github.com/jorgebucaran/nvm.fish[with fish]):

nvm use

=== Front-End

npm run dev
Empty file added api/__init__.py
Empty file.
45 changes: 45 additions & 0 deletions api/api_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Ceopardy
# https://github.com/obilodeau/ceopardy/
#
# Olivier Bilodeau <[email protected]>
# Copyright (C) 2024 Olivier Bilodeau
# All rights reserved.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
from flask import Blueprint, current_app as app, jsonify

api_bp = Blueprint('api', __name__, url_prefix='/api/v1')

@api_bp.route('/categories', methods=['GET'])
def get_categories():
categories = app.controller.get_categories()
return jsonify(categories)


@api_bp.route('/current_question', methods=['GET'])
def get_active_question():
return jsonify(app.controller.get_active_question())


@api_bp.route('/questions/grid', methods=['GET'])
def get_game_grid():
return jsonify(app.controller.get_questions_status_for_viewer())


@api_bp.route('/state', methods=['GET'])
def get_state():
return jsonify(app.controller.get_complete_state())


@api_bp.route('/scores', methods=['GET'])
def get_scores():
return jsonify(app.controller.get_teams_score())
78 changes: 43 additions & 35 deletions ceopardy.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@
import sys

from flask import g, Flask, render_template, redirect, jsonify, request
from flask_cors import CORS
from flask_socketio import SocketIO, emit, disconnect
from flask_sqlalchemy import SQLAlchemy

import utils
from api.api_routes import api_bp
from config import config
from forms import TeamNamesForm, TEAM_FIELD_ID

Expand All @@ -36,9 +38,12 @@
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + config['DATABASE_FILENAME']
# To supress warnings about a feature we don't use
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
# Register the API Blueprint
app.register_blueprint(api_bp)

socketio = SocketIO(app)
db = SQLAlchemy(app)
CORS(app)


@app.context_processor
Expand All @@ -50,12 +55,11 @@ def inject_config():
@app.route('/')
@app.route('/viewer')
def viewer():
controller = get_controller()
scores = controller.get_teams_score()
categories = controller.get_categories()
questions = controller.get_questions_status_for_viewer()
state = controller.get_complete_state()
active_question = controller.get_active_question()
scores = app.controller.get_teams_score()
categories = app.controller.get_categories()
questions = app.controller.get_questions_status_for_viewer()
state = app.controller.get_complete_state()
active_question = app.controller.get_active_question()
return render_template('viewer.html', scores=scores, categories=categories,
questions=questions, state=state,
active_question=active_question)
Expand All @@ -67,7 +71,7 @@ def viewer():
# TODO add authentication here
@app.route('/host')
def host():
controller = get_controller()
controller = app.controller

if not controller.is_game_in_progress():
must_init = controller.is_game_initialized() is False
Expand Down Expand Up @@ -104,7 +108,7 @@ def host():
@app.route('/init', methods=["POST"])
def init():
"""Init can resume a finished game or start a new one"""
controller = get_controller()
controller = app.controller

if request.form['action'] == "new":
roundfile = request.form['name']
Expand Down Expand Up @@ -152,7 +156,7 @@ def init():

@app.route('/setup', methods=["POST"])
def setup():
controller = get_controller()
controller = app.controller
form = TeamNamesForm()
# TODO: [LOW] csrf token errors are not logged (and return 200 which contradicts docs)
if not form.validate_on_submit():
Expand All @@ -169,7 +173,7 @@ def answer():
# FIXME this form isn't CSRF protected
app.logger.debug("Answer form has been submitted with: {}", request.form)
data = request.form
controller = get_controller()
controller = app.controller

app.logger.debug('received data: {}'.format(data["id"]))
try:
Expand Down Expand Up @@ -213,7 +217,7 @@ def answer():

@socketio.on('question', namespace='/host')
def handle_question(data):
controller = get_controller()
controller = app.controller
if data["action"] == "select":
col, row = utils.parse_question_id(data["id"])
question = controller.get_question(col, row)
Expand Down Expand Up @@ -261,7 +265,7 @@ def handle_question(data):

@socketio.on('message', namespace='/host')
def handle_message(data):
controller = get_controller()
controller = app.controller
# FIXME Temporary XSS!!!
if data["action"] == "show":
content = "<p>{0}</p>".format(data["text"])
Expand All @@ -279,7 +283,7 @@ def handle_message(data):

@socketio.on('team', namespace='/host')
def handle_team(data):
controller = get_controller()
controller = app.controller
if data["action"] == "select":
team = data["id"]
data["args"] = team
Expand All @@ -301,13 +305,12 @@ def handle_team(data):

@socketio.on('slider', namespace='/host')
def handle_slider(data):
controller = get_controller()
controller.set_state(data["id"], data["value"])
app.controller.set_state(data["id"], data["value"])


@socketio.on('final', namespace='/host')
def move_to_final_round(data):
controller = get_controller()
controller = app.controller
if controller.is_final_question():
# TODO implement
pass
Expand All @@ -322,34 +325,35 @@ def move_to_final_round(data):

@socketio.on('refresh', namespace='/viewer')
def handle_refresh():
controller = get_controller()
# FIXME
#state = controller.dictionize_questions_solved()
#state = app.controller.dictionize_questions_solved()
state = {}
emit("update-board", state)


if __name__ == '__main__':
def create_app():

# Logging
file_handler = logging.FileHandler('ceopardy.log')
#file_handler.setLevel(logging.INFO)
file_handler.setLevel(logging.DEBUG)
fmt = logging.Formatter(
'{asctime} {levelname}: {message} [in {pathname}:{lineno}]', style='{')
file_handler.setFormatter(fmt)
app.logger.addHandler(file_handler)

# Cleaner controller access
# Unsure if required once we have a db back-end
with app.app_context():

# Logging
file_handler = logging.FileHandler('ceopardy.log')
#file_handler.setLevel(logging.INFO)
file_handler.setLevel(logging.DEBUG)
fmt = logging.Formatter(
'{asctime} {levelname}: {message} [in {pathname}:{lineno}]', style='{')
file_handler.setFormatter(fmt)
app.logger.addHandler(file_handler)

# Database
app.db = db

# Controller
from controller import Controller
def get_controller():
_ctl = getattr(g, '_ctl', None)
if _ctl is None:
_ctl = g._ctl = Controller()
return _ctl
_ctl = getattr(g, '_ctl', None)
if _ctl is None:
_ctl = g._ctl = Controller()
# TODO would be nice if this could get type hints
app.controller = _ctl

@app.teardown_appcontext
def teardown_controller(exception):
Expand All @@ -361,3 +365,7 @@ def teardown_controller(exception):
# WARNING: This app is not ready to be exposed on the network.
# Game host interface would be exposed.
socketio.run(app, host="127.0.0.1", debug=True)


if __name__ == '__main__':
create_app()
11 changes: 8 additions & 3 deletions config.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@
#
import os

from dotenv import dotenv_values


config = {
'NB_TEAMS': 3,
'VARIABLE_TEAMS': False,
'CATEGORIES_PER_GAME': 5,
'QUESTIONS_PER_CATEGORY': 5,
'QUESTIONS_FILENAME': 'data/Questions.cp',
'SCORE_TICK': 100,
'MESSAGES': [\
{"title": "Game not started",
"text": "Please wait while the game is being set up..."},
Expand All @@ -40,3 +40,8 @@
'DAILYDOUBLE_WAIGER_MIN': 5,
'DAILYDOUBLE_WAIGER_MAX_MIN': 500
}

env_config = dotenv_values(".env")

# eventually we can replace with Python's 3.9+ config = config | env_config
config = {**config, **env_config}
Loading