diff --git a/.travis.yml b/.travis.yml index 53020c40..65fa7e3d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -41,7 +41,8 @@ before_script: script: - psql -c 'create database mpwo_test;' -U postgres - - docker-compose -f docker-compose-ci.yml run mpwo-api flask init_db + - docker-compose -f docker-compose-ci.yml run mpwo-api flask db migrate + - docker-compose -f docker-compose-ci.yml run mpwo-api flask db upgrade - sh test.sh after_script: diff --git a/Makefile b/Makefile index 1a60b062..17b5da91 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,10 @@ make-p: set -m; (for p in $(P); do ($(MAKE) $$p || kill 0)& done; wait) init-db: - $(FLASK) init_db + $(FLASK) drop_db + $(FLASK) db migrate + $(FLASK) db upgrade + $(FLASK) init_data install: install-client install-python @@ -26,6 +29,9 @@ lint-python: lint-react: $(NPM) lint +migrate-db: + $(FLASK) db migrate + serve-python: $(FLASK) run --with-threads -h $(HOST) -p $(API_PORT) @@ -40,3 +46,6 @@ test-e2e: test-python: $(FLASK) test_local + +upgrade-db: + $(FLASK) db upgrade diff --git a/migrations/README b/migrations/README new file mode 100755 index 00000000..98e4f9c4 --- /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 00000000..f8ed4801 --- /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 100755 index 00000000..23663ff2 --- /dev/null +++ b/migrations/env.py @@ -0,0 +1,87 @@ +from __future__ import with_statement +from alembic import context +from sqlalchemy import engine_from_config, pool +from logging.config import fileConfig +import logging + +# 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')) +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) + + 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.') + + engine = engine_from_config(config.get_section(config.config_ini_section), + prefix='sqlalchemy.', + poolclass=pool.NullPool) + + connection = engine.connect() + context.configure(connection=connection, + target_metadata=target_metadata, + process_revision_directives=process_revision_directives, + **current_app.extensions['migrate'].configure_args) + + try: + with context.begin_transaction(): + context.run_migrations() + finally: + connection.close() + +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 100755 index 00000000..2c015630 --- /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/9741fc7834da_.py b/migrations/versions/9741fc7834da_.py new file mode 100644 index 00000000..4f1f2f72 --- /dev/null +++ b/migrations/versions/9741fc7834da_.py @@ -0,0 +1,44 @@ +"""empty message + +Revision ID: 9741fc7834da +Revises: +Create Date: 2018-01-20 18:59:49.200032 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '9741fc7834da' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('users', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('username', sa.String(length=20), nullable=False), + sa.Column('email', sa.String(length=120), nullable=False), + sa.Column('password', sa.String(length=255), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('admin', sa.Boolean(), nullable=False), + sa.Column('first_name', sa.String(length=80), nullable=True), + sa.Column('last_name', sa.String(length=80), nullable=True), + sa.Column('birth_date', sa.DateTime(), nullable=True), + sa.Column('location', sa.String(length=80), nullable=True), + sa.Column('bio', sa.String(length=200), nullable=True), + sa.Column('picture', sa.String(length=255), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('email'), + sa.UniqueConstraint('username') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('users') + # ### end Alembic commands ### diff --git a/mpwo_api/mpwo_api/__init__.py b/mpwo_api/mpwo_api/__init__.py index 4ab4bf06..0e8f083d 100644 --- a/mpwo_api/mpwo_api/__init__.py +++ b/mpwo_api/mpwo_api/__init__.py @@ -2,11 +2,13 @@ import logging from flask import Flask from flask_bcrypt import Bcrypt +from flask_migrate import Migrate from flask_sqlalchemy import SQLAlchemy db = SQLAlchemy() bcrypt = Bcrypt() +migrate = Migrate() appLog = logging.getLogger('mpwo_api') # instantiate the app @@ -19,6 +21,7 @@ with app.app_context(): # set up extensions db.init_app(app) bcrypt.init_app(app) +migrate.init_app(app, db) from .users.auth import auth_blueprint # noqa from .users.users import users_blueprint # noqa diff --git a/mpwo_api/requirements.txt b/mpwo_api/requirements.txt index 5ae8b231..d3f181e8 100644 --- a/mpwo_api/requirements.txt +++ b/mpwo_api/requirements.txt @@ -6,6 +6,7 @@ flake8-isort==2.2.2 flake8-polyfill==1.0.1 Flask==0.12.2 Flask-Bcrypt==0.7.1 +Flask-Migrate==2.1.1 Flask-SQLAlchemy==2.3.2 Flask-Testing==0.6.2 isort==4.2.15 diff --git a/mpwo_api/server.py b/mpwo_api/server.py index 5f2e4218..f87a96f8 100644 --- a/mpwo_api/server.py +++ b/mpwo_api/server.py @@ -5,10 +5,15 @@ from mpwo_api.users.models import User @app.cli.command() -def init_db(): - """Init the database.""" +def drop_db(): db.drop_all() - db.create_all() + db.session.commit() + print('Database dropped.') + + +@app.cli.command() +def init_data(): + """Init the database.""" admin = User( username='admin', email='admin@example.com', @@ -16,7 +21,7 @@ def init_db(): admin.admin = True db.session.add(admin) db.session.commit() - print('Database initialization done.') + print('Admin created.') def run_test(test_path='mpwo_api/tests'): diff --git a/yarn.lock b/yarn.lock index f424870b..8ef78b02 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1495,7 +1495,7 @@ chalk@1.1.3, chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3: strip-ansi "^3.0.0" supports-color "^2.0.0" -chalk@^2.0.0, chalk@^2.1.0, chalk@^2.3.0: +chalk@2.3.0, chalk@^2.0.0, chalk@^2.1.0, chalk@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.0.tgz#b5ea48efc9c1793dccc9b4767c93914d3f2d52ba" dependencies: @@ -1684,7 +1684,7 @@ concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" -concat-stream@^1.6.0: +concat-stream@^1.4.7, concat-stream@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7" dependencies: @@ -3285,10 +3285,6 @@ hash.js@^1.0.0, hash.js@^1.0.3: inherits "^2.0.3" minimalistic-assert "^1.0.0" -hashids@^1.1.1: - version "1.1.4" - resolved "https://registry.yarnpkg.com/hashids/-/hashids-1.1.4.tgz#e4ff92ad66b684a3bd6aace7c17d66618ee5fa21" - hawk@3.1.3, hawk@~3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4" @@ -4921,6 +4917,10 @@ os-locale@^2.0.0: lcid "^1.0.0" mem "^1.1.0" +os-shim@^0.1.2: + version "0.1.3" + resolved "https://registry.yarnpkg.com/os-shim/-/os-shim-0.1.3.tgz#6b62c3791cf7909ea35ed46e17658bb417cb3917" + os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.1, os-tmpdir@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" @@ -5424,6 +5424,14 @@ postcss@^6.0.0, postcss@^6.0.1, postcss@^6.0.13: source-map "^0.6.1" supports-color "^5.1.0" +pre-commit@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/pre-commit/-/pre-commit-1.2.2.tgz#dbcee0ee9de7235e57f79c56d7ce94641a69eec6" + dependencies: + cross-spawn "^5.0.1" + spawn-sync "^1.0.15" + which "1.2.x" + prelude-ls@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" @@ -5673,12 +5681,6 @@ react-helmet@^5.2.0: prop-types "^15.5.4" react-side-effect "^1.1.0" -react-key-index@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/react-key-index/-/react-key-index-0.1.1.tgz#8319e4f0961ae44a8eb0a4f76e4c210ef6d39cda" - dependencies: - hashids "^1.1.1" - react-redux@^5.0.6: version "5.0.6" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.0.6.tgz#23ed3a4f986359d68b5212eaaa681e60d6574946" @@ -6414,6 +6416,13 @@ source-map@^0.6.1, source-map@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" +spawn-sync@^1.0.15: + version "1.0.15" + resolved "https://registry.yarnpkg.com/spawn-sync/-/spawn-sync-1.0.15.tgz#b00799557eb7fb0c8376c29d44e8a1ea67e57476" + dependencies: + concat-stream "^1.4.7" + os-shim "^0.1.2" + spdx-correct@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40" @@ -7362,6 +7371,12 @@ which-promise@^1.0.0: pinkie-promise "^1.0.0" which "^1.1.2" +which@1.2.x: + version "1.2.14" + resolved "https://registry.yarnpkg.com/which/-/which-1.2.14.tgz#9a87c4378f03e827cecaf1acdf56c736c01c14e5" + dependencies: + isexe "^2.0.0" + which@^1.1.2, which@^1.2.12, which@^1.2.14, which@^1.2.9: version "1.3.0" resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a"