From 88dfbceac76ab571617bd1fa03c7d149b63c5532 Mon Sep 17 00:00:00 2001 From: Drew Fisher Date: Sun, 9 Mar 2014 22:53:39 -0700 Subject: [PATCH 1/1] Initial commit Flask skeleton, basic database schema, migration infrastructure. --- README.md | 11 +++ config.py | 4 + create_db_migrate_script.py | 49 ++++++++++ imoo/__init__.py | 58 +++++++++++ imoo/forms.py | 5 + imoo/migrations/README | 4 + imoo/migrations/manage.py | 5 + imoo/migrations/migrate.cfg | 25 +++++ imoo/migrations/versions/001_migration.py | 48 +++++++++ imoo/migrations/versions/__init__.py | 0 imoo/models.py | 114 ++++++++++++++++++++++ imoo/views.py | 10 ++ run.py | 6 ++ 13 files changed, 339 insertions(+) create mode 100644 README.md create mode 100644 config.py create mode 100644 create_db_migrate_script.py create mode 100644 imoo/__init__.py create mode 100644 imoo/forms.py create mode 100644 imoo/migrations/README create mode 100644 imoo/migrations/manage.py create mode 100644 imoo/migrations/migrate.cfg create mode 100644 imoo/migrations/versions/001_migration.py create mode 100644 imoo/migrations/versions/__init__.py create mode 100644 imoo/models.py create mode 100644 imoo/views.py create mode 100755 run.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..a68d793 --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +imoo + +A cow-erful web frontend to bitlbee. + +SETUP + +```bash +virtualenv env +env/bin/pip install Flask Flask-Login Flask-SQLAlchemy Flask-WTF Flask-Scrypt sqlalchemy-migrate +./run.py +``` diff --git a/config.py b/config.py new file mode 100644 index 0000000..0291ace --- /dev/null +++ b/config.py @@ -0,0 +1,4 @@ +import os + +basedir = os.path.abspath(os.path.dirname(__file__)) +SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'state', 'database.db') diff --git a/create_db_migrate_script.py b/create_db_migrate_script.py new file mode 100644 index 0000000..6249bb5 --- /dev/null +++ b/create_db_migrate_script.py @@ -0,0 +1,49 @@ +#!env/bin/python +# You have a model (A) that represents your current DBs. +# You want to transform that into a new model (B). +# +# To create the migration script: +# 1) Have (A) on the filesystem. +# 2) Populate the database with the schema described by version (A). +# This will be done if you simply run the app, since it auto-applies all +# migrations. +# 3) Edit models.py (A) into (B) on the filesystem. +# 4) Run this script with a description of the changes as argv[1]. + +import imp +import os.path +import sys + +from migrate.versioning import api + +from imoo import create_app, db, models + +app = create_app() + +SQLALCHEMY_DATABASE_URI = app.config['SQLALCHEMY_DATABASE_URI'] +SQLALCHEMY_MIGRATE_REPO = app.config['SQLALCHEMY_MIGRATE_REPO'] + +# come up with a name for the migration +migration_description = "migration" +if len(sys.argv) > 1: + migration_description = sys.argv[1].replace(' ', '_') + +# Pick the next filename for this script +current_db_version = api.db_version(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO) +new_migration_script = os.path.join(SQLALCHEMY_MIGRATE_REPO, "versions", + "{0:03d}_{1}.py".format(current_db_version + 1, migration_description)) + +# Get current model from DB, save into a temporary module +tmp_module = imp.new_module('old_model') +old_model = api.create_model(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO) +exec old_model in tmp_module.__dict__ + +# Generate a new update script and save it to disk +script = api.make_update_script_for_model(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO, tmp_module.meta, db.metadata) +with open(new_migration_script, "wt") as f: + f.write(script) + print script + +print "New migration saved as", new_migration_script +print "Current database version:", api.db_version(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO) +print "Run update_db.py to update your database to the new version" diff --git a/imoo/__init__.py b/imoo/__init__.py new file mode 100644 index 0000000..fda60a1 --- /dev/null +++ b/imoo/__init__.py @@ -0,0 +1,58 @@ +import os + +from flask import Flask +from flask.ext.login import LoginManager +from flask.ext.sqlalchemy import SQLAlchemy +from flask_wtf.csrf import CsrfProtect +from migrate.versioning import api +from migrate.exceptions import DatabaseAlreadyControlledError + +# Login manager. +login_manager = LoginManager() + +# CSRF protection +csrf = CsrfProtect() + +# Database stuff +db = SQLAlchemy() + +def migrate_database(app): + db_uri = app.config['SQLALCHEMY_DATABASE_URI'] + repo = app.config['SQLALCHEMY_MIGRATE_REPO'] + # If the DB isn't under version control yet, add the migrate_version table + # at version 0 + try: + api.version_control(db_uri, repo) + except DatabaseAlreadyControlledError: + # we already own the DB + pass + + # Apply all known migrations to bring the database schema up to date + api.upgrade(db_uri, repo, api.version(repo)) + +from imoo import views + +def create_app(): + app = Flask(__name__) + # Base configuration + app.config.from_object('config') + + # Enable plugins: + # 1) Flask-Login + # Disabled until the views are implemented + #login_namager.init_app(app) + #login_manager.login_view = '.login_page' + + # 2) Flask-WTF CSRF protection + csrf.init_app(app) + + # 3) Flask-SQLAlchemy + db.init_app(app) + _moddir = os.path.abspath(os.path.dirname(__file__)) + app.config['SQLALCHEMY_MIGRATE_REPO'] = os.path.join(_moddir, 'migrations') + + # Enable routes + app.register_blueprint(views.blueprint, url_prefix="") + + # Return configured app + return app diff --git a/imoo/forms.py b/imoo/forms.py new file mode 100644 index 0000000..27cb4d5 --- /dev/null +++ b/imoo/forms.py @@ -0,0 +1,5 @@ +from flask.ext.wtf import Form +from wtforms import TextField, PasswordField, IntegerField, DateField, SelectField, TextAreaField +from wtforms.validators import ValidationError, InputRequired, Email, Length, Optional + + diff --git a/imoo/migrations/README b/imoo/migrations/README new file mode 100644 index 0000000..6218f8c --- /dev/null +++ b/imoo/migrations/README @@ -0,0 +1,4 @@ +This is a database migration repository. + +More information at +http://code.google.com/p/sqlalchemy-migrate/ diff --git a/imoo/migrations/manage.py b/imoo/migrations/manage.py new file mode 100644 index 0000000..b62a9c3 --- /dev/null +++ b/imoo/migrations/manage.py @@ -0,0 +1,5 @@ +#!/usr/bin/env python +from migrate.versioning.shell import main + +if __name__ == "__main__": + main() diff --git a/imoo/migrations/migrate.cfg b/imoo/migrations/migrate.cfg new file mode 100644 index 0000000..35b15f2 --- /dev/null +++ b/imoo/migrations/migrate.cfg @@ -0,0 +1,25 @@ +[db_settings] +# Used to identify which repository this database is versioned under. +# You can use the name of your project. +repository_id=imoo + +# The name of the database table used to track the schema version. +# This name shouldn't already be used by your project. +# If this is changed once a database is under version control, you'll need to +# change the table name in each database too. +version_table=migrate_version + +# When committing a change script, Migrate will attempt to generate the +# sql for all supported databases; normally, if one of them fails - probably +# because you don't have that database installed - it is ignored and the +# commit continues, perhaps ending successfully. +# Databases in this list MUST compile successfully during a commit, or the +# entire commit will fail. List the databases your application will actually +# be using to ensure your updates to that database work properly. +# This must be a list; example: ['postgres','sqlite'] +required_dbs=['postgres', 'sqlite'] + +# When creating new change scripts, Migrate will stamp the new script with +# a version number. By default this is latest_version + 1. You can set this +# to 'true' to tell Migrate to use the UTC timestamp instead. +use_timestamp_numbering=False diff --git a/imoo/migrations/versions/001_migration.py b/imoo/migrations/versions/001_migration.py new file mode 100644 index 0000000..816d4a6 --- /dev/null +++ b/imoo/migrations/versions/001_migration.py @@ -0,0 +1,48 @@ +from sqlalchemy import * +from migrate import * + + +from migrate.changeset import schema +pre_meta = MetaData() +post_meta = MetaData() +chataccount = Table('chataccount', post_meta, + Column('id', Integer, primary_key=True, nullable=False), + Column('user_id', Integer, nullable=False), + Column('network', Integer, nullable=False), + Column('username', UnicodeText(length=256), nullable=False), + Column('credential', UnicodeText(length=256), nullable=False), +) + +prefs = Table('prefs', post_meta, + Column('id', Integer, primary_key=True, nullable=False), + Column('user_id', Integer, nullable=False), + Column('key', UnicodeText, nullable=False), + Column('value', LargeBinary, nullable=False), +) + +user = Table('user', post_meta, + Column('id', Integer, primary_key=True, nullable=False), + Column('username', UnicodeText(length=256), nullable=False), + Column('pw_salt', String(length=89), nullable=False), + Column('pw_hash', String(length=89), nullable=False), + Column('enc_salt', String(length=89), nullable=False), +) + + +def upgrade(migrate_engine): + # Upgrade operations go here. Don't create your own engine; bind + # migrate_engine to your metadata + pre_meta.bind = migrate_engine + post_meta.bind = migrate_engine + post_meta.tables['chataccount'].create() + post_meta.tables['prefs'].create() + post_meta.tables['user'].create() + + +def downgrade(migrate_engine): + # Operations to reverse the above upgrade go here. + pre_meta.bind = migrate_engine + post_meta.bind = migrate_engine + post_meta.tables['chataccount'].drop() + post_meta.tables['prefs'].drop() + post_meta.tables['user'].drop() diff --git a/imoo/migrations/versions/__init__.py b/imoo/migrations/versions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/imoo/models.py b/imoo/models.py new file mode 100644 index 0000000..fa5477f --- /dev/null +++ b/imoo/models.py @@ -0,0 +1,114 @@ +from flask.ext import scrypt + +from imoo import db + +# Maximum length for email addresses, in bytes, including null terminator +_EMAIL_MAX_LEN = 256 + +# Arbitrary limit for user-provided strings indended to bound resource usage +# from user strings +_USER_STRING_MAX_LEN = 256 + +# The size of 64 bytes of data base64'd, then given a null terminator +_SALT_LENGTH = 89 +_HASH_LENGTH = 89 + +class TimeStampedMixin(object): + # A semi-magical mixin class that provides two columns for tracking + # creation date and modification date of rows. It works by registering a + # couple of functions to be called just before INSERT or UPDATE queries are + # made to the backend DB. + + # To use this, simply subclass Model and TimeStampedMixin, and then call + # YourModel.register() to get auto-updating for all instances of the class. + + # Standard create/update timestamps for recordkeeping + # All timestamps should be stored in UTC. + create_date = db.Column(db.DateTime, nullable=False) + modify_date = db.Column(db.DateTime, nullable=False) + @staticmethod + def create_time(mapper, connection, instance): + now = datetime.datetime.utcnow() + instance.create_date = now + instance.modify_date = now + + @staticmethod + def update_time(mapper, connection, instance): + now = datetime.datetime.utcnow() + instance.modify_date = now + + @classmethod + def register(cls): + db.event.listen(cls, 'before_insert', cls.create_time) + db.event.listen(cls, 'before_update', cls.update_time) + +class User(db.Model): + __tablename__ = 'user' + + id = db.Column('id', db.Integer, primary_key=True, autoincrement=True) + # A user is a: + # username + username = db.Column('username', db.UnicodeText(length=_USER_STRING_MAX_LEN), unique=True, nullable=False) + # password + pw_salt = db.Column('pw_salt', db.String(_SALT_LENGTH), nullable=False) + pw_hash = db.Column('pw_hash', db.String(_HASH_LENGTH), nullable=False) + # A salt used with the user's actual password to derive a user-specific encryption + enc_salt = db.Column('enc_salt', db.String(_SALT_LENGTH), nullable=False) + # set of preferences + preferences = db.relationship('Preferences', backref='user', lazy='dynamic') + # set of chat network accounts + accounts = db.relationship("ChatNetworkAccount", backref='user', lazy='dynamic') + + def set_password(self, password): + # Generate a new random salt every time we set the password. + pw_salt = scrypt.generate_random_salt() + # scrypt her password with that salt + pw_hash = scrypt.generate_password_hash(password, pw_salt) + # Generate new encryption password + enc_salt = scrypt.generate_random_salt() + # TODO: decrypt/reencrypt all account passwords + self.pw_salt = pw_salt + self.pw_hash = pw_hash + self.enc_salt = enc_salt + + def __repr__(self): + return "".format(self.username) + + # The following four methods are used for Flask-Login integration: + def is_active(self): + return True + def is_authenticated(self): + return True + def is_anonymous(self): + return False + def get_id(self): + return self.id + +class ChatNetworkAccount(db.Model): + __tablename__ = 'chataccount' + chatnetworks = [ "oscar", "jabber" ] # Can add others later, but I only care about these two + + id = db.Column('id', db.Integer, primary_key=True, autoincrement=True) + # A chat network account is: + # bound to a User + user_id = db.Column('user_id', db.Integer, db.ForeignKey('user.id'), nullable=False) + # a chat network + network = db.Column('network', db.Integer, nullable=False) + # a username + username = db.Column('username', db.UnicodeText(length=_USER_STRING_MAX_LEN), nullable=False) + # an encrypted credential + credential = db.Column('credential', db.UnicodeText(length=_USER_STRING_MAX_LEN), nullable=False) + + def __repr__(self): + return "".format(self.username, chatnetworks[self.network]) + +class Preferences(db.Model): + # This is really messy, but extensible at the application layer... + __tablename__ = 'prefs' + id = db.Column('id', db.Integer, primary_key=True, autoincrement=True) + user_id = db.Column('user_id', db.Integer, db.ForeignKey('user.id'), nullable=False) + + # String key + key = db.Column('key', db.UnicodeText, nullable=False) + # Opaque blob data + value = db.Column('value', db.LargeBinary, nullable=False) diff --git a/imoo/views.py b/imoo/views.py new file mode 100644 index 0000000..2205630 --- /dev/null +++ b/imoo/views.py @@ -0,0 +1,10 @@ +from flask import Blueprint + +from imoo import db, login_manager +from . import forms, models + +blueprint = Blueprint('main', __name__, template_folder='templates') + +@blueprint.route("/") +def test(): + return "

HI THERE

" diff --git a/run.py b/run.py new file mode 100755 index 0000000..dc716da --- /dev/null +++ b/run.py @@ -0,0 +1,6 @@ +#!env/bin/python +from imoo import create_app, migrate_database + +app = create_app() +migrate_database(app) +app.run(debug=True) -- 2.39.5