Add working code
This commit is contained in:
parent
0a396fed59
commit
63b2fc084f
3
.gitignore
vendored
3
.gitignore
vendored
@ -129,3 +129,6 @@ dmypy.json
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# Project stuff
|
||||
fishbowl/fishbowl.db
|
||||
data/
|
||||
|
25
Dockerfile
Normal file
25
Dockerfile
Normal file
@ -0,0 +1,25 @@
|
||||
FROM python:3
|
||||
|
||||
RUN mkdir -p /app
|
||||
WORKDIR /app
|
||||
|
||||
# Install service runtime requirements
|
||||
COPY ./requirements-minimal.txt /app/requirements.txt
|
||||
RUN pip install -r ./requirements.txt
|
||||
|
||||
RUN mkdir /data
|
||||
VOLUME /data
|
||||
ENV DB_URI sqlite:////data/fishbowl.db
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
# Install service as package for alembic
|
||||
COPY ./fishbowl /app/fishbowl
|
||||
|
||||
# Own app dir and drop root
|
||||
RUN chown -R www-data:www-data /app
|
||||
RUN chown -R www-data:www-data /data
|
||||
USER www-data
|
||||
|
||||
|
||||
CMD ["python", "/app/fishbowl/app.py"]
|
9
Makefile
Normal file
9
Makefile
Normal file
@ -0,0 +1,9 @@
|
||||
.PHONY: default clean test all
|
||||
|
||||
default: venv
|
||||
env FLASK_ENV=development ./venv/bin/python ./fishbowl/app.py
|
||||
|
||||
|
||||
venv:
|
||||
virtualenv -p python3 ./venv
|
||||
./venv/bin/pip install -r ./requirements-minimal.txt
|
7
docker-compose.prod.yml
Normal file
7
docker-compose.prod.yml
Normal file
@ -0,0 +1,7 @@
|
||||
---
|
||||
version: '2'
|
||||
services:
|
||||
main:
|
||||
environment:
|
||||
FLASK_ENV: production
|
||||
SECRET_KEY: ${SECRET_KEY}
|
12
docker-compose.yml
Normal file
12
docker-compose.yml
Normal file
@ -0,0 +1,12 @@
|
||||
---
|
||||
version: '2'
|
||||
services:
|
||||
main:
|
||||
build: .
|
||||
ports:
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
- ./data:/data
|
||||
environment:
|
||||
FLASK_ENV: development
|
||||
SECRET_KEY: mysupersecretkey
|
176
fishbowl/app.py
Normal file
176
fishbowl/app.py
Normal file
@ -0,0 +1,176 @@
|
||||
from uuid import uuid4
|
||||
from random import choice
|
||||
from os import environ
|
||||
import crypt
|
||||
|
||||
from flask import Flask
|
||||
from flask import redirect
|
||||
from flask import render_template
|
||||
from flask import request
|
||||
from flask import session
|
||||
from flask import url_for
|
||||
from flask_bootstrap import Bootstrap
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, SubmitField, BooleanField, PasswordField
|
||||
from wtforms.validators import DataRequired, Length, Optional
|
||||
import flask_sqlalchemy
|
||||
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = environ.get(
|
||||
'DB_URI', 'sqlite:///fishbowl.db'
|
||||
)
|
||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||
app.secret_key = environ.get('SECRET_KEY', uuid4().hex)
|
||||
bootstrap = Bootstrap(app)
|
||||
db = SQLAlchemy(app)
|
||||
|
||||
|
||||
class Game(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
uuid = db.Column(db.String(32), unique=True, nullable=False)
|
||||
password = db.Column(db.String(128))
|
||||
admin_password = db.Column(db.String(128))
|
||||
words = db.relationship(
|
||||
'Word',
|
||||
backref=db.backref('game', lazy=True),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def new(cls, password, admin_password=None):
|
||||
game = Game(uuid=uuid4().hex)
|
||||
salt = crypt.mksalt()
|
||||
if password:
|
||||
game.password = crypt.crypt(password, salt)
|
||||
if admin_password:
|
||||
admin_password = crypt.crypt(admin_password, salt)
|
||||
return game
|
||||
|
||||
@classmethod
|
||||
def by_uuid(cls, uuid):
|
||||
return cls.query.filter_by(
|
||||
uuid=uuid,
|
||||
).one()
|
||||
|
||||
def get_url(self):
|
||||
return url_for('.game', game_uuid=self.uuid)
|
||||
|
||||
def get_remaining_words(self):
|
||||
return [
|
||||
word for word in self.words
|
||||
if not word.is_hidden and not word.is_picked
|
||||
]
|
||||
|
||||
|
||||
class Word(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
text = db.Column(db.String(1024))
|
||||
game_id = db.Column(db.Integer, db.ForeignKey('game.id'))
|
||||
is_hidden = db.Column(db.Boolean, default=False)
|
||||
is_picked = db.Column(db.Boolean, default=False)
|
||||
|
||||
|
||||
class NewGameForm(FlaskForm):
|
||||
game_password = PasswordField(
|
||||
'Game password (optional)', validators=[Optional(), Length(5, 128)]
|
||||
)
|
||||
# admin_password = PasswordField(
|
||||
# 'Admin password', validators=[DataRequired(), Length(5, 128)]
|
||||
# )
|
||||
submit = SubmitField(label='Create')
|
||||
|
||||
|
||||
class GameLoginForm(FlaskForm):
|
||||
game_password = PasswordField(
|
||||
'Game password', validators=[DataRequired(), Length(5, 128)]
|
||||
)
|
||||
# admin_password = PasswordField(
|
||||
# 'Admin password', validators=[DataRequired(), Length(5, 128)]
|
||||
# )
|
||||
submit = SubmitField(label='Login')
|
||||
|
||||
|
||||
class AddWordForm(FlaskForm):
|
||||
new_word = StringField('New word', validators=[Length(3, 1024)])
|
||||
submit_new_word = SubmitField(label='Add new word')
|
||||
|
||||
|
||||
class PlayGameForm(FlaskForm):
|
||||
draw_word = SubmitField(label='Draw word')
|
||||
reset_game = SubmitField(label='Reset game')
|
||||
reset_confirm = BooleanField(label='Confirm reset')
|
||||
|
||||
|
||||
@app.route('/', methods=['GET', 'POST'])
|
||||
def index():
|
||||
form = NewGameForm()
|
||||
if request.method == 'POST':
|
||||
if form.validate_on_submit():
|
||||
game = Game.new(form.game_password.data)
|
||||
db.session.add(game)
|
||||
db.session.commit()
|
||||
app.logger.info("Created game %i, %s", game.id, game.uuid)
|
||||
return redirect(game.get_url())
|
||||
else:
|
||||
print("Form was not valid")
|
||||
|
||||
return render_template('index.html', form=form)
|
||||
|
||||
|
||||
@app.route('/game/<game_uuid>', methods=['GET', 'POST'])
|
||||
def game(game_uuid):
|
||||
# Init login tracker
|
||||
if 'logged_in' not in session:
|
||||
session['logged_in'] = {}
|
||||
game_login = GameLoginForm()
|
||||
word_form = AddWordForm()
|
||||
play_form = PlayGameForm()
|
||||
try:
|
||||
game = Game.by_uuid(game_uuid)
|
||||
except flask_sqlalchemy.orm.exc.NoResultFound:
|
||||
return redirect(url_for('.index'))
|
||||
word = None
|
||||
if request.method == 'POST':
|
||||
# Check if trying to login
|
||||
if game_login.submit.data and game_login.validate():
|
||||
if crypt.crypt(game.password, game_login.game_password.data):
|
||||
if not session['logged_in']:
|
||||
session['logged_in'] = {}
|
||||
session['logged_in'][game.uuid] = True
|
||||
# Check if word is being added
|
||||
if word_form.submit_new_word.data and word_form.validate():
|
||||
word = Word(text=word_form.new_word.data)
|
||||
game.words.append(word)
|
||||
# We just drew a new word!
|
||||
if play_form.draw_word.data:
|
||||
words = game.get_remaining_words()
|
||||
if words:
|
||||
word = choice(words)
|
||||
word.is_picked = True
|
||||
else:
|
||||
word = "There are no words. Add some and try again!"
|
||||
# Reset the game
|
||||
if play_form.reset_game.data and play_form.reset_confirm.data:
|
||||
for unpick_word in game.words:
|
||||
unpick_word.is_picked = False
|
||||
|
||||
db.session.add(game)
|
||||
db.session.commit()
|
||||
|
||||
words = game.get_remaining_words()
|
||||
|
||||
return render_template(
|
||||
'game.html',
|
||||
game_login=game_login,
|
||||
word_form=word_form,
|
||||
play_form=play_form,
|
||||
game=game,
|
||||
word=word,
|
||||
words=words,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
db.create_all()
|
||||
app.run(host='0.0.0.0', port=3000)
|
BIN
fishbowl/fishbowl.db
Normal file
BIN
fishbowl/fishbowl.db
Normal file
Binary file not shown.
36
fishbowl/templates/base.html
Normal file
36
fishbowl/templates/base.html
Normal file
@ -0,0 +1,36 @@
|
||||
{% from 'bootstrap/nav.html' import render_nav_item %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<title>Fishbowl</title>
|
||||
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
||||
{{ bootstrap.load_css() }}
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-4">
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent"
|
||||
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav mr-auto">
|
||||
{{ render_nav_item('index', 'Home', use_li=True) }}
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="container">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<footer class="text-center">
|
||||
{% block footer %}
|
||||
{% endblock %}
|
||||
</footer>
|
||||
|
||||
{{ bootstrap.load_js() }}
|
||||
</body>
|
||||
</html>
|
18
fishbowl/templates/game.html
Normal file
18
fishbowl/templates/game.html
Normal file
@ -0,0 +1,18 @@
|
||||
{% extends 'base.html' %}
|
||||
{% from 'bootstrap/form.html' import render_form %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Play game</h1>
|
||||
{% if game.password is not none and not session['logged_in'][game.uuid] %}
|
||||
{{ render_form(game_login) }}
|
||||
{% else %}
|
||||
<h3>To share this game, just share the url to this page</h3>
|
||||
{% if word is not none %}
|
||||
<div><span>Your word is: {{ word.text }}</span></div>
|
||||
{% endif %}
|
||||
<div><span>Words remaining: {{ words|length }}</span></div>
|
||||
{{ render_form(play_form) }}
|
||||
<h1>Add new word</h1>
|
||||
{{ render_form(word_form) }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
8
fishbowl/templates/index.html
Normal file
8
fishbowl/templates/index.html
Normal file
@ -0,0 +1,8 @@
|
||||
{% extends 'base.html' %}
|
||||
{% from 'bootstrap/form.html' import render_form %}
|
||||
|
||||
{% block content %}
|
||||
<h1>New game</h1>
|
||||
{{ render_form(form) }}
|
||||
{% endblock %}
|
||||
|
5
requirements-dev-minimal.txt
Normal file
5
requirements-dev-minimal.txt
Normal file
@ -0,0 +1,5 @@
|
||||
coverage
|
||||
docker-compose
|
||||
flake8
|
||||
pre-commit
|
||||
requirements-tools
|
6
requirements-minimal.txt
Normal file
6
requirements-minimal.txt
Normal file
@ -0,0 +1,6 @@
|
||||
flask
|
||||
flask-sqlalchemy
|
||||
bootstrap-flask
|
||||
flask-wtf
|
||||
sqlalchemy
|
||||
gunicorn
|
Loading…
Reference in New Issue
Block a user