]> git.zarvox.org Git - imoo.git/commitdiff
Initial commit
authorDrew Fisher <drew@aerofs.com>
Mon, 10 Mar 2014 05:53:39 +0000 (22:53 -0700)
committerDrew Fisher <drew@aerofs.com>
Mon, 10 Mar 2014 05:53:39 +0000 (22:53 -0700)
Flask skeleton, basic database schema, migration infrastructure.

13 files changed:
README.md [new file with mode: 0644]
config.py [new file with mode: 0644]
create_db_migrate_script.py [new file with mode: 0644]
imoo/__init__.py [new file with mode: 0644]
imoo/forms.py [new file with mode: 0644]
imoo/migrations/README [new file with mode: 0644]
imoo/migrations/manage.py [new file with mode: 0644]
imoo/migrations/migrate.cfg [new file with mode: 0644]
imoo/migrations/versions/001_migration.py [new file with mode: 0644]
imoo/migrations/versions/__init__.py [new file with mode: 0644]
imoo/models.py [new file with mode: 0644]
imoo/views.py [new file with mode: 0644]
run.py [new file with mode: 0755]

diff --git a/README.md b/README.md
new file mode 100644 (file)
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 (file)
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 (file)
index 0000000..6249bb5
--- /dev/null
@@ -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 (file)
index 0000000..fda60a1
--- /dev/null
@@ -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 (file)
index 0000000..27cb4d5
--- /dev/null
@@ -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 (file)
index 0000000..6218f8c
--- /dev/null
@@ -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 (file)
index 0000000..b62a9c3
--- /dev/null
@@ -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 (file)
index 0000000..35b15f2
--- /dev/null
@@ -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 (file)
index 0000000..816d4a6
--- /dev/null
@@ -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 (file)
index 0000000..e69de29
diff --git a/imoo/models.py b/imoo/models.py
new file mode 100644 (file)
index 0000000..fa5477f
--- /dev/null
@@ -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 "<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)
diff --git a/imoo/views.py b/imoo/views.py
new file mode 100644 (file)
index 0000000..2205630
--- /dev/null
@@ -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 "<!doctype html><html><body><h1>HI THERE</h1></body></html>"
diff --git a/run.py b/run.py
new file mode 100755 (executable)
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)