From f882ff13ff2bc3ad91a4b3d393000534afd26c28 Mon Sep 17 00:00:00 2001 From: Maxime Beauchemin Date: Fri, 4 Dec 2015 11:52:32 -0800 Subject: [PATCH] Logging slice and dash views --- .../versions/315b3f4da9b0_adding_log_model.py | 30 +++++++++++++++++++ panoramix/models.py | 13 +++++++- panoramix/utils.py | 24 +++++++++++++++ panoramix/views.py | 18 +++++++++++ 4 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 panoramix/migrations/versions/315b3f4da9b0_adding_log_model.py diff --git a/panoramix/migrations/versions/315b3f4da9b0_adding_log_model.py b/panoramix/migrations/versions/315b3f4da9b0_adding_log_model.py new file mode 100644 index 0000000000000..789f46fb41df0 --- /dev/null +++ b/panoramix/migrations/versions/315b3f4da9b0_adding_log_model.py @@ -0,0 +1,30 @@ +"""adding log model + +Revision ID: 315b3f4da9b0 +Revises: 1a48a5411020 +Create Date: 2015-12-04 11:16:58.226984 + +""" + +# revision identifiers, used by Alembic. +revision = '315b3f4da9b0' +down_revision = '1a48a5411020' + +from alembic import op +import sqlalchemy as sa + +def upgrade(): + op.create_table('logs', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('action', sa.String(length=512), nullable=True), + sa.Column('user_id', sa.Integer(), nullable=True), + sa.Column('json', sa.Text(), nullable=True), + sa.Column('dttm', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['ab_user.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.drop_table('birth_names') + + +def downgrade(): + op.drop_table('logs') diff --git a/panoramix/models.py b/panoramix/models.py index c8527aa1a2595..6ee1974a4fba7 100644 --- a/panoramix/models.py +++ b/panoramix/models.py @@ -9,7 +9,7 @@ import sqlalchemy as sqla from sqlalchemy import ( Column, Integer, String, ForeignKey, Text, Boolean, DateTime, - Table, create_engine, MetaData, desc, select, and_) + Table, create_engine, MetaData, desc, select, and_, func) from sqlalchemy.orm import relationship from sqlalchemy.sql import table, literal_column, text from sqlalchemy.sql.elements import ColumnClause @@ -885,6 +885,17 @@ def query( duration=datetime.now() - qry_start_dttm) +class Log(Model): + __tablename__ = 'logs' + + id = Column(Integer, primary_key=True) + action = Column(String(512)) + user_id = Column(Integer, ForeignKey('ab_user.id')) + json = Column(Text) + user = relationship('User', backref='logs', foreign_keys=[user_id]) + dttm = Column(DateTime, default=func.now()) + + class Metric(Model): __tablename__ = 'metrics' id = Column(Integer, primary_key=True) diff --git a/panoramix/utils.py b/panoramix/utils.py index eba7aaa18735e..01d197aab6cf6 100644 --- a/panoramix/utils.py +++ b/panoramix/utils.py @@ -3,6 +3,7 @@ import hashlib from sqlalchemy.types import TypeDecorator, TEXT import json +from flask import g, request import parsedatetime import functools from panoramix import db @@ -178,3 +179,26 @@ def init(): table.perm for table in session.query(models.Datasource).all()] for table_perm in table_perms: merge_perm(sm, 'datasource_access', table.perm) + + +def log_this(f): + ''' + Decorator to log user actions + ''' + @functools.wraps(f) + def wrapper(*args, **kwargs): + user_id = None + if g.user: + user_id = g.user.id + from panoramix import models + log = models.Log( + action=f.__name__, + json=json.dumps(request.args.to_dict()), + user_id=user_id) + + db.session.add(log) + db.session.commit() + + return f(*args, **kwargs) + + return wrapper diff --git a/panoramix/views.py b/panoramix/views.py index ab5e50a406945..dc9972780d072 100644 --- a/panoramix/views.py +++ b/panoramix/views.py @@ -1,6 +1,7 @@ from datetime import datetime import json import logging +import re import traceback from flask import request, redirect, flash, Response, render_template @@ -228,6 +229,8 @@ class DashboardModelView(PanoramixModelView, DeleteMixin): } def pre_add(self, obj): obj.slug = obj.slug.strip() or None + obj.slug = obj.slug.replace(" ", "-") + obj.slug = re.sub(r'\W+', '', obj.slug) def pre_update(self, obj): self.pre_add(obj) @@ -241,6 +244,19 @@ def pre_update(self, obj): category_icon='',) +class LogModelView(PanoramixModelView): + datamodel = SQLAInterface(models.Log) + list_columns = ('user', 'action', 'dttm') + edit_columns = ('user', 'action', 'dttm', 'json') + base_order = ('dttm','desc') + +appbuilder.add_view( + LogModelView, + "Action Log", + category="Security", + icon="fa-list-ol") + + class DatasourceModelView(PanoramixModelView, DeleteMixin): datamodel = SQLAInterface(models.Datasource) list_columns = [ @@ -285,6 +301,7 @@ class Panoramix(BaseView): @has_access @expose("/explore///") @expose("/datasource///") # Legacy url + @utils.log_this def explore(self, datasource_type, datasource_id): if datasource_type == "table": datasource = ( @@ -432,6 +449,7 @@ def testconn(self): @has_access @expose("/dashboard//") + @utils.log_this def dashboard(self, identifier): session = db.session() qry = session.query(models.Dashboard)