diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app.py b/app.py index d7c5e88..8e7d1be 100644 --- a/app.py +++ b/app.py @@ -1,10 +1,18 @@ import os from flask import Flask + +# ! SQLAlchemy > Marshmallow - these must be imported in this order from flask_sqlalchemy import SQLAlchemy from flask_marshmallow import Marshmallow +from flask_migrate import Migrate + +from flask_bcrypt import Bcrypt +from flask_cors import CORS + app = Flask(__name__) +CORS(app) # base directory basedir = os.path.abspath(os.path.dirname(__file__)) @@ -16,12 +24,26 @@ DATABASE = 'postgresql://localhost/browser-go' app.config['SQLALCHEMY_DATABASE_URI'] = DATABASE app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False +#init bcrypt +bcrypt = Bcrypt(app) + # init database db = SQLAlchemy(app) # init marshmallow ma = Marshmallow(app) +# init all db models +from .models.User import User +from .models.GameRoom import GameRoom +from .models.TimeSettings import TimeSettings +from .models.Game import Game +from .models.Move import Move +from .models.Message import Message + +migrate = Migrate(app, db) + + # dev server DEBUG = True PORT = 8000 diff --git a/games/__init__.py b/games/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/games/game.py b/games/game.py new file mode 100644 index 0000000..3f289de --- /dev/null +++ b/games/game.py @@ -0,0 +1,7 @@ +from flask import Blueprint + +game = Blueprint('games', __name__) + +@game.route('/') +def func(): + pass \ No newline at end of file diff --git a/home/__init__.py b/home/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/home/home.py b/home/home.py new file mode 100644 index 0000000..22fa366 --- /dev/null +++ b/home/home.py @@ -0,0 +1,7 @@ +from flask import Blueprint + +home = Blueprint('home', __name__) + +@home.route('/home') +def func(): + pass \ No newline at end of file diff --git a/migrations/README b/migrations/README new file mode 100644 index 0000000..98e4f9c --- /dev/null +++ b/migrations/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/migrations/alembic.ini b/migrations/alembic.ini new file mode 100644 index 0000000..f8ed480 --- /dev/null +++ b/migrations/alembic.ini @@ -0,0 +1,45 @@ +# A generic, single database configuration. + +[alembic] +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/migrations/env.py b/migrations/env.py new file mode 100644 index 0000000..79b8174 --- /dev/null +++ b/migrations/env.py @@ -0,0 +1,96 @@ +from __future__ import with_statement + +import logging +from logging.config import fileConfig + +from sqlalchemy import engine_from_config +from sqlalchemy import pool + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) +logger = logging.getLogger('alembic.env') + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +from flask import current_app +config.set_main_option( + 'sqlalchemy.url', current_app.config.get( + 'SQLALCHEMY_DATABASE_URI').replace('%', '%%')) +target_metadata = current_app.extensions['migrate'].db.metadata + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, target_metadata=target_metadata, literal_binds=True + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + # this callback is used to prevent an auto-migration from being generated + # when there are no changes to the schema + # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html + def process_revision_directives(context, revision, directives): + if getattr(config.cmd_opts, 'autogenerate', False): + script = directives[0] + if script.upgrade_ops.is_empty(): + directives[:] = [] + logger.info('No changes in schema detected.') + + connectable = engine_from_config( + config.get_section(config.config_ini_section), + prefix='sqlalchemy.', + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=target_metadata, + process_revision_directives=process_revision_directives, + **current_app.extensions['migrate'].configure_args + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/migrations/script.py.mako b/migrations/script.py.mako new file mode 100644 index 0000000..2c01563 --- /dev/null +++ b/migrations/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/migrations/versions/16c70b11242e_.py b/migrations/versions/16c70b11242e_.py new file mode 100644 index 0000000..af28b38 --- /dev/null +++ b/migrations/versions/16c70b11242e_.py @@ -0,0 +1,28 @@ +"""empty message + +Revision ID: 16c70b11242e +Revises: 9519d28f13f0 +Create Date: 2019-10-02 14:50:27.180692 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '16c70b11242e' +down_revision = '9519d28f13f0' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('moves', sa.Column('is_main', sa.Boolean(), nullable=False)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('moves', 'is_main') + # ### end Alembic commands ### diff --git a/migrations/versions/9519d28f13f0_.py b/migrations/versions/9519d28f13f0_.py new file mode 100644 index 0000000..1f327ae --- /dev/null +++ b/migrations/versions/9519d28f13f0_.py @@ -0,0 +1,109 @@ +"""empty message + +Revision ID: 9519d28f13f0 +Revises: +Create Date: 2019-10-02 13:54:17.877590 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '9519d28f13f0' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('game_rooms', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('name', sa.String(length=40), nullable=False), + sa.Column('description', sa.String(length=200), nullable=False), + sa.Column('private', sa.Boolean(), nullable=False), + sa.Column('language', sa.Enum(name='languages'), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('time_settings', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('main_time', sa.Enum('BYOYOMI', 'ABSOLUTE', 'HOURGLASS', 'NONE', name='timetypes'), nullable=False), + sa.Column('time_period', sa.Integer(), nullable=True), + sa.Column('period_length', sa.Integer(), nullable=True), + sa.Column('overtime', sa.Enum('BYOYOMI', 'ABSOLUTE', 'HOURGLASS', 'NONE', name='timetypes'), nullable=False), + sa.Column('overtime_period', sa.Integer(), nullable=True), + sa.Column('overtime_length', sa.Integer(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('users', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('email', sa.String(length=255), nullable=False), + sa.Column('password', sa.DateTime(), nullable=False), + sa.Column('registered_on', sa.DateTime(), nullable=False), + sa.Column('admin', sa.Boolean(), nullable=False), + sa.Column('rank', sa.Enum('D7', 'D6', 'D5', 'D4', 'D3', 'D2', 'D1', 'K1', 'K2', 'K3', 'K4', 'K5', 'K6', 'K7', 'K8', 'K9', 'K10', 'K11', 'K12', 'K13', 'K14', 'K15', 'K16', 'K17', 'K18', 'K19', 'K20', 'K21', 'K22', 'K23', 'K24', 'K25', 'K26', 'K27', 'K28', 'K29', 'K30', name='ranks'), nullable=True), + sa.Column('elo', sa.Integer(), nullable=True), + sa.Column('rank_certainty', sa.Boolean(), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('email') + ) + op.create_table('games', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('date', sa.DateTime(), nullable=True), + sa.Column('komi', sa.Numeric(precision=2, scale=1), nullable=False), + sa.Column('handicap', sa.Integer(), nullable=False), + sa.Column('board_size', sa.Integer(), nullable=False), + sa.Column('win_type', sa.Enum('DRAW', 'RESIGN', 'SCORE', 'TIME', 'VOID', name='wintype'), nullable=True), + sa.Column('winner', sa.Enum('BLACK', 'WHITE', 'VOID', name='players'), nullable=True), + sa.Column('score', sa.Numeric(precision=2, scale=1), nullable=True), + sa.Column('white_captures', sa.Integer(), nullable=True), + sa.Column('black_captures', sa.Integer(), nullable=True), + sa.Column('application', sa.String(length=40), nullable=True), + sa.Column('application_version', sa.String(length=20), nullable=True), + sa.Column('event', sa.String(length=40), nullable=True), + sa.Column('name', sa.String(length=40), nullable=True), + sa.Column('description', sa.String(length=200), nullable=True), + sa.Column('round', sa.Integer(), nullable=True), + sa.Column('game_room', sa.Integer(), nullable=True), + sa.Column('time_settings', sa.Integer(), nullable=True), + sa.Column('player_black', sa.Integer(), nullable=True), + sa.Column('player_white', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['game_room'], ['game_rooms.id'], ), + sa.ForeignKeyConstraint(['player_black'], ['users.id'], ), + sa.ForeignKeyConstraint(['player_white'], ['users.id'], ), + sa.ForeignKeyConstraint(['time_settings'], ['time_settings.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('moves', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('player', sa.Enum('BLACK', 'WHITE', name='players'), nullable=True), + sa.Column('x_point', sa.Integer(), nullable=True), + sa.Column('y_point', sa.Integer(), nullable=True), + sa.Column('move_number', sa.Integer(), nullable=True), + sa.Column('game', sa.Integer(), nullable=False), + sa.Column('preceding_move', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['game'], ['games.id'], ), + sa.ForeignKeyConstraint(['preceding_move'], ['moves.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('messages', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('date', sa.DateTime(), nullable=False), + sa.Column('content', sa.String(length=200), nullable=False), + sa.Column('move', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['move'], ['moves.id'], ), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('messages') + op.drop_table('moves') + op.drop_table('games') + op.drop_table('users') + op.drop_table('time_settings') + op.drop_table('game_rooms') + # ### end Alembic commands ### diff --git a/models/Game.py b/models/Game.py new file mode 100644 index 0000000..f8ec676 --- /dev/null +++ b/models/Game.py @@ -0,0 +1,44 @@ +from ..app import db, ma +import enum + +class Players(enum.Enum): + BLACK = "The player taking black stones" + WHITE = "The player taking white stones" + VOID = "The game was a draw or voided" + +class WinType(enum.Enum): + DRAW = "The game is a draw" + RESIGN = "The game ended in resignation" + SCORE = "The game ended by counting points" + TIME = "The game ended in loss by time out" + VOID = "The game was suspended" + +class Game(db.Model): + __tablename__ = "games" + __table_args__ = {'extend_existing': True} + + id = db.Column(db.Integer, primary_key=True, autoincrement=True) + date = db.Column(db.DateTime()) + komi = db.Column(db.Numeric(2,1), nullable=False) + handicap = db.Column(db.Integer, nullable=False) + board_size = db.Column(db.Integer, nullable=False) + win_type = db.Column(db.Enum(WinType)) + winner = db.Column(db.Enum(Players)) + score = db.Column(db.Numeric(2,1)) + white_captures = db.Column(db.Integer) + black_captures = db.Column(db.Integer) + application = db.Column(db.String(40)) + application_version = db.Column(db.String(20)) + event = db.Column(db.String(40)) + name = db.Column(db.String(40)) + description = db.Column(db.String(200)) + round = db.Column(db.Integer) + + # foreign keys + game_room = db.Column(db.Integer, db.ForeignKey("game_rooms.id")) + time_settings = db.Column(db.Integer, db.ForeignKey("time_settings.id")) + player_black = db.Column(db.Integer, db.ForeignKey("users.id")) + player_white = db.Column(db.Integer, db.ForeignKey("users.id")) + + def __init__(self): + pass \ No newline at end of file diff --git a/models/GameRoom.py b/models/GameRoom.py new file mode 100644 index 0000000..5da8f4e --- /dev/null +++ b/models/GameRoom.py @@ -0,0 +1,18 @@ +from ..app import db, ma +import enum + +class Languages(enum.Enum): + EN: "English" + +class GameRoom(db.Model): + __tablename__ = "game_rooms" + __table_args__ = {'extend_existing': True} + + id = db.Column(db.Integer, primary_key=True, autoincrement=True) + name = db.Column(db.String(40), nullable=False) + description = db.Column(db.String(200), nullable=False) + private = db.Column(db.Boolean(), nullable=False, default=False) + language = db.Column(db.Enum(Languages), nullable=False) + + def __init__(self): + pass \ No newline at end of file diff --git a/models/Message.py b/models/Message.py new file mode 100644 index 0000000..7a273a4 --- /dev/null +++ b/models/Message.py @@ -0,0 +1,20 @@ +from ..app import db, ma +import enum + +class Players(enum.Enum): + BLACK = "The player taking black stones" + WHITE = "The player taking white stones" + +class Message(db.Model): + __tablename__ = "messages" + __table_args__ = {'extend_existing': True} + + id = db.Column(db.Integer, primary_key=True, autoincrement=True) + date = db.Column(db.DateTime(), nullable=False) + content = db.Column(db.String(200), nullable=False) + + # foreign key + move = db.Column(db.Integer, db.ForeignKey("moves.id"), nullable=False) + + def __init__(self): + pass \ No newline at end of file diff --git a/models/Move.py b/models/Move.py new file mode 100644 index 0000000..d3dd264 --- /dev/null +++ b/models/Move.py @@ -0,0 +1,25 @@ +from ..app import db, ma +import enum + +class Players(enum.Enum): + BLACK = "The player taking black stones" + WHITE = "The player taking white stones" + +class Move(db.Model): + __tablename__ = "moves" + __table_args__ = {'extend_existing': True} + + id = db.Column(db.Integer, primary_key=True, autoincrement=True) + player = db.Column(db.Enum(Players)) + x_point = db.Column(db.Integer) + y_point = db.Column(db.Integer) + move_number = db.Column(db.Integer) + is_pass = db.Column(db.Boolean, nullable=False, default=False) + is_main = db.Column(db.Boolean, nullable=False, default=True) + + # foreign keys + game = db.Column(db.Integer, db.ForeignKey("games.id"), nullable=False) + preceding_move = db.Column(db.Integer, db.ForeignKey("moves.id")) + + def __init__(self): + pass \ No newline at end of file diff --git a/models/TimeSettings.py b/models/TimeSettings.py new file mode 100644 index 0000000..bdf142e --- /dev/null +++ b/models/TimeSettings.py @@ -0,0 +1,23 @@ +from ..app import db, ma +import enum + +class TimeTypes(enum.Enum): + BYOYOMI = "Counting by time period" + ABSOLUTE = "One period to use time" + HOURGLASS = "Absolute time for both players" + NONE = "Untimed" + +class TimeSettings(db.Model): + __table_args__ = {'extend_existing': True} + + id = db.Column(db.Integer, primary_key=True, autoincrement=True) + main_time = db.Column(db.Enum(TimeTypes), nullable=False) + time_period = db.Column(db.Integer) # number of periods + period_length = db.Column(db.Integer) # seconds + overtime = db.Column(db.Enum(TimeTypes), nullable=False) + overtime_period = db.Column(db.Integer) # number of overtime periods + overtime_length = db.Column(db.Integer) # seconds + + + def __init__(self): + pass \ No newline at end of file diff --git a/models/User.py b/models/User.py new file mode 100644 index 0000000..b2888eb --- /dev/null +++ b/models/User.py @@ -0,0 +1,58 @@ +from ..app import db, ma, bcrypt +import enum + +class Ranks(enum.Enum): # with minimal Elo rating + D7 = "Seven Dan" # Elo 2700+ + D6 = "Six Dan" + D5 = "Five Dan" # Elo 2500 + D4 = "Four Dan" + D3 = "Three Dan" + D2 = "Two Dan" + D1 = "One Dan" + K1 = "One Kyu" # Elo 2000 + K2 = "Two Kyu" + K3 = "Three Kyu" + K4 = "Four Kyu" + K5 = "Five Kyu" + K6 = "Six Kyu" # Elo 1500 + K7 = "Seven Kyu" + K8 = "Eight Kyu" + K9 = "Nine Kyu" + K10 = "Ten Kyu" + K11 = "Eleven Kyu" # ELo 1000 + K12 = "Twelve Kyu" + K13 = "Thirteen Kyu" + K14 = "Fourteen Kyu" + K15 = "Fifteen Kyu" + K16 = "Sixteen Kyu" # Elo 500 + K17 = "Seventeen Kyu" + K18 = "Eighteen Kyu" + K19 = "Nineteen Kyu" + K20 = "Twenty Kyu" + K21 = "Twenty-One Kyu" # Elo 0 + K22 = "Twenty-Two Kyu" + K23 = "Twenty-Three Kyu" + K24 = "Twenty-Four Kyu" + K25 = "Twenty-Five Kyu" + K26 = "Twenty-Six Kyu" # Elo -500 + K27 = "Twenty-Seven Kyu" + K28 = "Twenty-Eight Kyu" + K29 = "Twenty-Nine Kyu" + K30 = "Thirty Kyu" # Elo -900 + +class User(db.Model): + __tablename__ = "users" + + id = db.Column(db.Integer, primary_key=True, autoincrement=True) + email = db.Column(db.String(255), unique=True, nullable=False) + password = db.Column(db.DateTime, nullable=False) + registered_on = db.Column(db.DateTime, nullable=False) + admin = db.Column(db.Boolean, nullable=False, default=False) + rank = db.Column(db.Enum(Ranks)) + elo = db.Column(db.Integer) + rank_certainty = db.Column(db.Boolean, nullable=False, default=False) + + def __init__(self): + self.password = bcrypt.generate_password_hash( + password, app.config.get('BCRYPT_LOG_ROUNDS') + ).decode() diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/requirements.txt b/requirements.txt index 56ac562..547158f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,13 +1,31 @@ +alembic==1.2.1 +astroid==2.3.1 +bcrypt==3.1.7 +cffi==1.12.3 Click==7.0 Flask==1.1.1 +Flask-Bcrypt==0.7.1 Flask-Cors==3.0.8 flask-marshmallow==0.10.1 +Flask-Migrate==2.5.2 Flask-SQLAlchemy==2.4.1 +isort==4.3.21 itsdangerous==1.1.0 Jinja2==2.10.1 +lazy-object-proxy==1.4.2 +Mako==1.1.0 MarkupSafe==1.1.1 marshmallow==3.2.0 marshmallow-sqlalchemy==0.19.0 +mccabe==0.6.1 +numpy==1.17.2 +psycopg2==2.8.3 +pycparser==2.19 +pylint==2.4.2 +python-dateutil==2.8.0 +python-editor==1.0.4 six==1.12.0 SQLAlchemy==1.3.8 +typed-ast==1.4.0 Werkzeug==0.16.0 +wrapt==1.11.2 diff --git a/rooms/__init__.py b/rooms/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/rooms/room.py b/rooms/room.py new file mode 100644 index 0000000..13081d1 --- /dev/null +++ b/rooms/room.py @@ -0,0 +1,7 @@ +from flask import Blueprint + +room = Blueprint('rooms', __name__) + +@room.route('/') +def func(): + pass \ No newline at end of file diff --git a/users/__init__.py b/users/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/users/user.py b/users/user.py new file mode 100644 index 0000000..fd9b6d3 --- /dev/null +++ b/users/user.py @@ -0,0 +1,11 @@ +from flask import Blueprint + +user = Blueprint('users', __name__) + +@user.route('/') +def func(): + pass + +@user.route('/') +def func(): + pass \ No newline at end of file