From 6be6064741a0def99b6e88e7f6aaeb5fd3b9c759 Mon Sep 17 00:00:00 2001
From: Benjamin Bertrand <benjamin.bertrand@esss.se>
Date: Fri, 25 Aug 2017 11:56:34 +0200
Subject: [PATCH] Add items versioning with SQLAlchemy-Continuum

---
 app/models.py   | 13 ++++++++++++
 app/plugins.py  | 54 +++++++++++++++++++++++++++++++++++++++++++++++++
 environment.yml |  2 ++
 3 files changed, 69 insertions(+)
 create mode 100644 app/plugins.py

diff --git a/app/models.py b/app/models.py
index e988370..8e74079 100644
--- a/app/models.py
+++ b/app/models.py
@@ -11,16 +11,20 @@ This module implements the models used in the app.
 """
 import re
 import qrcode
+import sqlalchemy as sa
 from sqlalchemy.orm import validates
 from sqlalchemy.ext.associationproxy import association_proxy
+from sqlalchemy_continuum import make_versioned
 from citext import CIText
 from flask import current_app
 from flask_login import UserMixin
 from .extensions import db, login_manager, ldap_manager, jwt
+from .plugins import FlaskUserPlugin
 from . import utils
 
 
 ICS_ID_RE = re.compile('[A-Z]{3}[0-9]{3}')
+make_versioned(plugins=[FlaskUserPlugin()])
 
 
 @login_manager.user_loader
@@ -183,6 +187,10 @@ class Status(QRCodeMixin, db.Model):
 
 
 class Item(db.Model):
+    __versioned__ = {
+        'exclude': ['_created']
+    }
+
     id = db.Column(db.Integer, primary_key=True)
     _created = db.Column(db.DateTime, default=db.func.now())
     _updated = db.Column(db.DateTime, default=db.func.now(), onupdate=db.func.now())
@@ -231,3 +239,8 @@ class Item(db.Model):
             'updated': utils.format_field(self._updated),
             'created': utils.format_field(self._created),
         }
+
+
+# call configure_mappers after defining all the models
+# required by sqlalchemy_continuum
+sa.orm.configure_mappers()
diff --git a/app/plugins.py b/app/plugins.py
new file mode 100644
index 0000000..d51cd96
--- /dev/null
+++ b/app/plugins.py
@@ -0,0 +1,54 @@
+# -*- coding: utf-8 -*-
+"""
+app.plugins
+~~~~~~~~~~~
+
+This module implements SQLAlchemy-Continuum plugins.
+
+FlaskUserPlugin offers way of integrating Flask framework with
+SQLAlchemy-Continuum. FlaskUser-Plugin adds a `user_id` column for Transaction model.
+
+This plugin is based on the official FlaskPlugin with the following modifications:
+    - no remote_addr column
+    - the user_id is taken from flask_jwt_extended (API) or flask_login (web UI)
+
+The `user_id` column is automatically populated when the transaction object is created.
+
+:copyright: original (c) 2012, Konsta Vesterinen
+:copyright: modified (c) 2017 European Spallation Source ERIC
+:license: BSD 2-Clause, see LICENSE for more details.
+
+"""
+
+from flask.globals import _app_ctx_stack, _request_ctx_stack
+from sqlalchemy_continuum.plugins import Plugin
+from flask_login import current_user
+from flask_jwt_extended import get_current_user
+
+
+def fetch_current_user_id():
+    # Return None if we are outside of request context.
+    if _app_ctx_stack.top is None or _request_ctx_stack.top is None:
+        return None
+    # Try to get the user from both flask_jwt_extended and flask_login
+    user = get_current_user() or current_user
+    try:
+        return user.id
+    except AttributeError:
+        return None
+
+
+class FlaskUserPlugin(Plugin):
+    def __init__(
+        self,
+        current_user_id_factory=None,
+    ):
+        self.current_user_id_factory = (
+            fetch_current_user_id if current_user_id_factory is None
+            else current_user_id_factory
+        )
+
+    def transaction_args(self, uow, session):
+        return {
+            'user_id': self.current_user_id_factory(),
+        }
diff --git a/environment.yml b/environment.yml
index ff90d44..cd652c5 100644
--- a/environment.yml
+++ b/environment.yml
@@ -60,4 +60,6 @@ dependencies:
   - pyasn1==0.2.3
   - pyjwt==1.4.2
   - sqlalchemy-citext==1.3.post0
+  - sqlalchemy-continuum==1.3.1
+  - sqlalchemy-utils==0.32.14
   - visitor==0.1.3
-- 
GitLab