Usage

The sections below detail how to fully use this module, along with context for design decisions made during development of the plugin.

Setup

Obviously, this plugin requires the use of SQLAlchemy for model definitions. However, there are two common patterns for how SQLAlchemy models are configured for a Flask application:

  1. Using the Flask-SQLAlchemy plugin for simplifying boilerplate associated with configuring a SQLAlchemy-backed Flask application (recommended).
  2. Using SQLAlchemy directly with the declarative system for defining models in your application.

If you’re using the Flask-SQLAlchemy plugin, you can configure this plugin by passing the db parameter into the extension:

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_continuum import Continuum

db = SQLAlchemy()
continuum = Continuum(db=db)
app = Flask(__name__)
db.init_app(app)
continuum.init_app(app)

If you’re using SQLAlchemy directly, you need to pass the SQLAlchemy engine to the plugin. See the SQLAlchemy documentation for more context on setting up the engine:

from flask import Flask
from sqlalchemy import create_engine
from flask_continuum import Continuum

engine = create_engine('postgresql://admin:password@localhost:5432/my-database')
continuum = Continuum(engine=engine)
app = Flask(__name__)
continuum.init_app(app)

Aside from the plugin configuration detailed above, there is no additional steps required for configuring mappers or setting up sqlalchemy-continuum. SQLAlchemy mappers for versioning tables will be set up when the first connection to the application database is made. For more information on additional configuration options, see the Other Customizations section below.

Mixins

In order to add versioning support to models in your application, you can either:

  1. Use the VersioningMixin from this package to add versioning support and additional helper methods (recommended).
  2. Add a __versioned__ = {} property to model classes.

With the VersioningMixin, you can add versioning to a model via:

class Article(db.Model, VersioningMixin):
    __tablename__ = 'article'

    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    name = db.Column(db.Unicode(255))
    content = db.Column(db.UnicodeText)
    updated_at = db.Column(db.DateTime, default=datetime.now)
    created_at = db.Column(db.DateTime, onupdate=datetime.now)

Additionally, if you only want to track specific fields in the database (for more efficient changeset processing), you can use the following syntax:

class Article(db.Model, VersioningMixin):
    __versioned__ = {
        'include': ['name', 'content']
    }
    __tablename__ = 'article'

    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    name = db.Column(db.Unicode(255))
    content = db.Column(db.UnicodeText)
    updated_at = db.Column(db.DateTime, default=datetime.now)
    created_at = db.Column(db.DateTime, onupdate=datetime.now)

For more details on what the __versioned__ property can encode, see the SQLAlchemy-Continuum documentation. If you have no need for the VersioningMixin, you can take route (2) like so:

class Article(db.Model):
    __versioned__ = {}
    __tablename__ = 'article'

    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    name = db.Column(db.Unicode(255))
    content = db.Column(db.UnicodeText)

Migrations

If you’re using alembic or Flask-Migrate alongside this tool, you need to make sure a flask application context is pushed before you create new migrations. Otherwise, database fields dynamically added by the Mixins above won’t be picked up by the migration tool.

If you’re using alembic directly, you’ll need to manually configure mappers in your app script or create_app factory after models are declared:

app = Flask(__name__)
db = SQLAlchemy(app)
continuum = Continuum(app, db)

class Article(db.Model, VersioningMixin):

    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    name = db.Column(db.Unicode(255))

continuum.configure()

If you’re using Flask-Migrate to manage migrations, you don’t need to manually configure the orm with versioning extensions. You can simply pass an instantiated Flask-Migrate plugin to Flask-Continuum:

app = Flask(__name__)
db = SQLAlchemy(app)
migrate = Migrate(app, db)
continuum = Continuum(app, db, migrate)

class Article(db.Model, VersioningMixin):

    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    name = db.Column(db.Unicode(255))

This will automatically configure mappers before Flask-Migrate performs any migration tasks.

Troubleshooting

>>> article = Article()
>>> db.session.add(article)
>>> db.session.commit()
...
OperationalError: no such table: transaction

This is usually an error caused when database tables haven’t been created before a commit is made. Make sure you create database tables with db.create_all() before trying to commit any data to the database.

~$ flask db migrate
...
INFO  [alembic.env] No changes in schema detected.

This alembic message is produced when alembic tries to create a new database migration but doesn’t detect any changes in SQLAlchemy models when trying to auto-generate the migration. It’s usually caused by an application context not being pushed before migrations take place. See the Migrations section for information on resolving this issue.

Other Customizations

As detailed in the Overview section of the documentation, the plugin can be customized with specific triggers. The following detail what can be customized:

  • user_cls - The name of the user table to associate with content changes.
  • current_user - A function for returning the current user issuing a request. By default, this is determined from the Flask-Login plugin, but can be overwritten.
  • engine - A SQLAlchemy engine to connect to the database. This parameter can be used if the application doesn’t require the use of Flask-SQLAlchemy.

The code below details how you can override all of these configuration options:

from flask import Flask
from flask_continuum import Continuum
from sqlalchemy import create_engine

app = Flask(__name__)
engine = create_engine('postgresql://...')
continuum = Continuum(
    engine=engine,
    user_cls='Users',
    current_user=lambda: g.user
)
continuum.init_app(app)

For even more in-depth information on the module and the tools it provides, see the API section of the documentation.