From 0aaead590294718dec444124e86466cccdf629ae Mon Sep 17 00:00:00 2001 From: Joshua McKenty Date: Sun, 30 Jun 2013 15:21:11 -0700 Subject: [PATCH] Added schema migrations and an admin view --- .gitignore | 2 + Procfile | 1 + alembic.ini | 50 +++++++++++++ alembic/README | 1 + alembic/env.py | 79 +++++++++++++++++++++ alembic/script.py.mako | 22 ++++++ alembic/versions/2e26571834ea_first_step.py | 33 +++++++++ {static => refstack/static}/toast.css | 0 refstack/web.py | 54 +++++++++++--- 9 files changed, 233 insertions(+), 9 deletions(-) create mode 100644 Procfile create mode 100644 alembic.ini create mode 100644 alembic/README create mode 100644 alembic/env.py create mode 100644 alembic/script.py.mako create mode 100644 alembic/versions/2e26571834ea_first_step.py rename {static => refstack/static}/toast.css (100%) diff --git a/.gitignore b/.gitignore index d2d6f360..2d45ddff 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,5 @@ nosetests.xml .mr.developer.cfg .project .pydevproject + +*.db \ No newline at end of file diff --git a/Procfile b/Procfile new file mode 100644 index 00000000..98db9fce --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: gunicorn refstack.web:app \ No newline at end of file diff --git a/alembic.ini b/alembic.ini new file mode 100644 index 00000000..848a3873 --- /dev/null +++ b/alembic.ini @@ -0,0 +1,50 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = 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 + +sqlalchemy.url = driver://user:pass@localhost/dbname + + +# 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/alembic/README b/alembic/README new file mode 100644 index 00000000..98e4f9c4 --- /dev/null +++ b/alembic/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/alembic/env.py b/alembic/env.py new file mode 100644 index 00000000..03aa4599 --- /dev/null +++ b/alembic/env.py @@ -0,0 +1,79 @@ +from __future__ import with_statement + +import os +import sys +sys.path.append("./") + + +from alembic import context +from sqlalchemy import engine_from_config, pool +from logging.config import fileConfig +from refstack.web import app + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config +cur_db_uri = config.get_section_option('alembic', 'sqlalchemy.url') +my_db_uri = app.config.get('SQLALCHEMY_DATABASE_URI', cur_db_uri) +config.set_section_option('alembic', 'sqlalchemy.url', my_db_uri) + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +# target_metadata = None +from refstack.web import db +target_metadata = db.metadata + + +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. + + """ + 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 + ) + + 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/alembic/script.py.mako b/alembic/script.py.mako new file mode 100644 index 00000000..95702017 --- /dev/null +++ b/alembic/script.py.mako @@ -0,0 +1,22 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision} +Create Date: ${create_date} + +""" + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/alembic/versions/2e26571834ea_first_step.py b/alembic/versions/2e26571834ea_first_step.py new file mode 100644 index 00000000..eda18ca8 --- /dev/null +++ b/alembic/versions/2e26571834ea_first_step.py @@ -0,0 +1,33 @@ +"""First step + +Revision ID: 2e26571834ea +Revises: None +Create Date: 2013-06-30 15:07:31.746984 + +""" + +# revision identifiers, used by Alembic. +revision = '2e26571834ea' +down_revision = None + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.create_table('vendor', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('vendor_name', sa.String(length=80), nullable=True), + sa.Column('contact_email', sa.String(length=120), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('contact_email'), + sa.UniqueConstraint('vendor_name') + ) + ### end Alembic commands ### + + +def downgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.drop_table('vendor') + ### end Alembic commands ### diff --git a/static/toast.css b/refstack/static/toast.css similarity index 100% rename from static/toast.css rename to refstack/static/toast.css diff --git a/refstack/web.py b/refstack/web.py index dd891d3e..999d3551 100644 --- a/refstack/web.py +++ b/refstack/web.py @@ -7,30 +7,66 @@ import os import random import sqlite3 import sys -from flask import Flask, request, render_template, g, jsonify +from flask import Flask, flash, request, redirect, url_for, render_template, g, session from flask.ext.sqlalchemy import SQLAlchemy -from contextlib import closing +from flask.ext.admin import Admin, BaseView, expose +from flask.ext.admin.contrib.sqlamodel import ModelView +from sqlalchemy.exc import IntegrityError +from flask.ext.security import Security, SQLAlchemyUserDatastore, \ + UserMixin, RoleMixin, login_required +from wtforms import Form, BooleanField, TextField, PasswordField, validators +from flask_mail import Mail +import requests app = Flask(__name__) -app.debug = True -app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////var/www/refstack/database.db' + +app.config['MAILGUN_KEY'] = 'key-7o9l9dupikfpsdvqi0ewot-se8g1hz64' +app.config['MAILGUN_DOMAIN'] = 'hastwoparents.com' + + +app.config['SECRET_KEY'] = 'GIANT_UGLY-SECRET-GOES-H3r3' +db_path = os.path.abspath(os.path.join(os.path.basename(__file__), "../")) +app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DATABASE_URL', 'sqlite:///%s/refstack.db' % (db_path)) +app.config['DEBUG'] = True + +app.config['SECURITY_PASSWORD_HASH'] = 'sha512_crypt' +app.config['SECURITY_PASSWORD_SALT'] = app.config['SECRET_KEY'] +app.config['SECURITY_POST_LOGIN_VIEW'] = 'dashboard' +app.config['SECURITY_RECOVERABLE'] = True +app.config['SECURITY_REGISTERABLE'] = True +app.config['SECURITY_EMAIL_SENDER'] = "admin@hastwoparents.com" + +app.config['MAIL_SERVER'] = 'smtp.mailgun.org' +app.config['MAIL_PORT'] = 465 +app.config['MAIL_USE_SSL'] = True +app.config['MAIL_USERNAME'] = 'postmaster@hastwoparents.com' +app.config['MAIL_PASSWORD'] = '0sx00qlvqbo3' + +mail = Mail(app) db = SQLAlchemy(app) + class Vendor(db.Model): id = db.Column(db.Integer, primary_key=True) vendor_name = db.Column(db.String(80), unique=True) contact_email = db.Column(db.String(120), unique=True) - def __init__(self, vendor_name, contact_email): - self.vendor_name = vendor_name - self.contact_email = contact_email - def __repr__(self): return '' % self.vendor_name +admin = Admin(app) +admin.add_view(ModelView(Vendor, db.session)) + + @app.route('/', methods=['POST','GET']) def index(): vendors = Vendor.query.all() - return render_template('index.html', vendors = vendors) \ No newline at end of file + return render_template('index.html', vendors = vendors) + + +if __name__ == '__main__': + app.logger.setLevel('DEBUG') + port = int(os.environ.get('PORT', 5000)) + app.run(host='0.0.0.0', port=port, debug=True)