44import itertools
55import json
66import logging
7- import time
87
98from funcy import project
109from flask_sqlalchemy import SQLAlchemy
1110from flask .ext .sqlalchemy import SignallingSession
1211from flask_login import UserMixin , AnonymousUserMixin
1312from sqlalchemy .dialects import postgresql
14- from sqlalchemy .event import listens_for
15- from sqlalchemy .inspection import inspect
13+ from sqlalchemy .event import listens_for , listen
1614from sqlalchemy .types import TypeDecorator
1715from sqlalchemy .orm import object_session
1816# noinspection PyUnresolvedReferences
@@ -84,50 +82,61 @@ def process_result_value(self, value, dialect):
8482
8583class TimestampMixin (object ):
8684 updated_at = Column (db .DateTime (True ), default = db .func .now (),
87- onupdate = db .func .now (), nullable = False )
85+ onupdate = db .func .now (), nullable = False )
8886 created_at = Column (db .DateTime (True ), default = db .func .now (),
89- nullable = False )
87+ nullable = False )
9088
9189
9290class ChangeTrackingMixin (object ):
93- skipped_fields = ('id' , 'created_at' , 'updated_at' , 'version' )
94- _clean_values = None
95-
96- def __init__ (self , * a , ** kw ):
97- super (ChangeTrackingMixin , self ).__init__ (* a , ** kw )
98- self .record_changes (self .user )
99-
100- def prep_cleanvalues (self ):
101- self .__dict__ ['_clean_values' ] = {}
102- for attr in inspect (self .__class__ ).column_attrs :
103- col , = attr .columns
104- # 'query' is col name but not attr name
105- self ._clean_values [col .name ] = None
106-
107- def __setattr__ (self , key , value ):
108- if self ._clean_values is None :
109- self .prep_cleanvalues ()
110- for attr in inspect (self .__class__ ).column_attrs :
111- col , = attr .columns
112- previous = getattr (self , attr .key , None )
113- self ._clean_values [col .name ] = previous
114-
115- super (ChangeTrackingMixin , self ).__setattr__ (key , value )
116-
117- def record_changes (self , changed_by ):
118- db .session .add (self )
119- db .session .flush ()
91+ @classmethod
92+ def after_change_listener (cls , mapper , connection , target ):
93+ state = db .inspect (target )
12094 changes = {}
121- for attr in inspect (self .__class__ ).column_attrs :
122- col , = attr .columns
123- if attr .key not in self .skipped_fields :
124- changes [col .name ] = {'previous' : self ._clean_values [col .name ],
125- 'current' : getattr (self , attr .key )}
12695
127- db .session .add (Change (object = self ,
128- object_version = self .version ,
129- user = changed_by ,
130- change = changes ))
96+ for attr in state .attrs :
97+ if attr .key not in cls .tracked_columns :
98+ continue
99+
100+ hist = state .get_history (attr .key , True )
101+
102+ if not hist .has_changes ():
103+ continue
104+
105+ if hist .deleted :
106+ previous = hist .deleted [0 ]
107+ else :
108+ previous = None
109+
110+ changes [attr .key ] = {
111+ 'previous' : previous ,
112+ 'current' : attr .value
113+ }
114+
115+ if changes :
116+ changed_by = cls .fetch_current_user_id () or target .user_id
117+ db .session .add (Change (object = target ,
118+ object_version = target .version ,
119+ user_id = changed_by ,
120+ change = changes ))
121+
122+ @staticmethod
123+ def fetch_current_user_id ():
124+ from flask_login import current_user
125+ from flask import has_app_context , has_request_context
126+
127+ # Return None if we are outside of request context.
128+ if not has_app_context () or not has_request_context ():
129+ return
130+ try :
131+ return current_user .id
132+ except AttributeError :
133+ return
134+
135+ @classmethod
136+ def __declare_last__ (cls ):
137+ # get called after mappings are completed
138+ listen (cls , 'after_update' , cls .after_change_listener )
139+ listen (cls , 'after_insert' , cls .after_change_listener )
131140
132141
133142class BelongsToOrgMixin (object ):
@@ -612,6 +621,9 @@ def should_schedule_next(previous_iteration, now, schedule):
612621
613622
614623class Query (ChangeTrackingMixin , TimestampMixin , BelongsToOrgMixin , db .Model ):
624+ tracked_columns = ('data_source_id' , 'latest_query_data_id' , 'name' , 'description' , 'query_text' , 'user_id' ,
625+ 'is_archived' , 'is_draft' , 'schedule' , 'options' )
626+
615627 id = Column (db .Integer , primary_key = True )
616628 version = Column (db .Integer )
617629 org_id = Column (db .Integer , db .ForeignKey ('organizations.id' ))
@@ -684,7 +696,7 @@ def to_dict(self, with_stats=False, with_visualizations=False, with_user=True, w
684696
685697 return d
686698
687- def archive (self , user = None ):
699+ def archive (self ):
688700 db .session .add (self )
689701 self .is_archived = True
690702 self .schedule = None
@@ -696,9 +708,6 @@ def archive(self, user=None):
696708 for a in self .alerts :
697709 db .session .delete (a )
698710
699- if user :
700- self .record_changes (user )
701-
702711 @classmethod
703712 def all_queries (cls , groups , drafts = False ):
704713 q = (cls .query .join (User , Query .user_id == User .id )
@@ -798,20 +807,6 @@ def fork(self, user):
798807 db .session .add (forked_query )
799808 return forked_query
800809
801- def update_instance_tracked (self , changing_user , old_object = None , * args , ** kwargs ):
802- self .version += 1
803- self .update_instance (* args , ** kwargs )
804- # save Change record
805- new_change = Change .save_change (user = changing_user , old_object = old_object , new_object = self )
806- return new_change
807-
808- def tracked_save (self , changing_user , old_object = None , * args , ** kwargs ):
809- self .version += 1
810- self .save (* args , ** kwargs )
811- # save Change record
812- new_change = Change .save_change (user = changing_user , old_object = old_object , new_object = self )
813- return new_change
814-
815810 @property
816811 def runtime (self ):
817812 return self .latest_query_data .runtime
@@ -831,6 +826,8 @@ def __unicode__(self):
831826 return unicode (self .id )
832827
833828
829+
830+
834831@listens_for (Query .query_text , 'set' )
835832def gen_query_hash (target , val , oldval , initiator ):
836833 target .query_hash = utils .gen_query_hash (val )
@@ -950,10 +947,6 @@ def to_dict(self, full=True):
950947
951948 return d
952949
953- @classmethod
954- def log_change (cls , changed_by , obj ):
955- return cls .create (object = obj , object_version = obj .version , user = changed_by , change = obj .changes )
956-
957950 @classmethod
958951 def last_change (cls , obj ):
959952 return db .session .query (cls ).filter (
@@ -1050,6 +1043,8 @@ def generate_slug(ctx):
10501043
10511044
10521045class Dashboard (ChangeTrackingMixin , TimestampMixin , BelongsToOrgMixin , db .Model ):
1046+ tracked_columns = ('slug' , 'name' , 'user_id' , 'layout' , 'dashboard_filters_enabled' , 'is_archived' , 'is_draft' )
1047+
10531048 id = Column (db .Integer , primary_key = True )
10541049 version = Column (db .Integer )
10551050 org_id = Column (db .Integer , db .ForeignKey ("organizations.id" ))
0 commit comments