Flask skeleton, basic database schema, migration infrastructure.
--- /dev/null
+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
+```
--- /dev/null
+import os
+
+basedir = os.path.abspath(os.path.dirname(__file__))
+SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'state', 'database.db')
--- /dev/null
+#!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"
--- /dev/null
+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
--- /dev/null
+from flask.ext.wtf import Form
+from wtforms import TextField, PasswordField, IntegerField, DateField, SelectField, TextAreaField
+from wtforms.validators import ValidationError, InputRequired, Email, Length, Optional
+
+
--- /dev/null
+This is a database migration repository.
+
+More information at
+http://code.google.com/p/sqlalchemy-migrate/
--- /dev/null
+#!/usr/bin/env python
+from migrate.versioning.shell import main
+
+if __name__ == "__main__":
+ main()
--- /dev/null
+[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
--- /dev/null
+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()
--- /dev/null
+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 "<User '{}'>".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 "<ChatNetworkAccount '{}' on '{}'>".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)
--- /dev/null
+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 "<!doctype html><html><body><h1>HI THERE</h1></body></html>"
--- /dev/null
+#!env/bin/python
+from imoo import create_app, migrate_database
+
+app = create_app()
+migrate_database(app)
+app.run(debug=True)