diff --git a/caravel/migrations/versions/ad82a75afd82_add_query_model.py b/caravel/migrations/versions/ad82a75afd82_add_query_model.py index 31e41afbada8..2d5c99551b28 100644 --- a/caravel/migrations/versions/ad82a75afd82_add_query_model.py +++ b/caravel/migrations/versions/ad82a75afd82_add_query_model.py @@ -35,6 +35,7 @@ def upgrade(): sa.Column('rows', sa.Integer(), nullable=True), sa.Column('error_message', sa.Text(), nullable=True), sa.Column('start_time', sa.DateTime(), nullable=True), + sa.Column('changed_on', sa.DateTime(), nullable=True), sa.Column('end_time', sa.DateTime(), nullable=True), sa.ForeignKeyConstraint(['database_id'], [u'dbs.id'], ), sa.ForeignKeyConstraint(['user_id'], [u'ab_user.id'], ), @@ -42,8 +43,14 @@ def upgrade(): ) op.add_column('dbs', sa.Column('select_as_create_table_as', sa.Boolean(), nullable=True)) + op.create_index( + op.f('ti_user_id_changed_on'), + 'query', ['user_id', 'changed_on'], unique=False) def downgrade(): + op.drop_index(op.f('ix_query_user_id'), table_name='query') + op.drop_index(op.f('ix_query_changed_on'), table_name='query') op.drop_table('query') op.drop_column('dbs', 'select_as_create_table_as') + diff --git a/caravel/models.py b/caravel/models.py index 6931beeb778a..d23c506aefb4 100644 --- a/caravel/models.py +++ b/caravel/models.py @@ -1768,7 +1768,8 @@ class Query(Model): # Store the tmp table into the DB only if the user asks for it. tmp_table_name = Column(String(256)) - user_id = Column(Integer, ForeignKey('ab_user.id'), nullable=True) + user_id = Column( + Integer, ForeignKey('ab_user.id'), nullable=True) # models.QueryStatus status = Column(String(16)) @@ -1794,3 +1795,26 @@ class Query(Model): error_message = Column(Text) start_time = Column(DateTime) end_time = Column(DateTime) + changed_on = Column( + DateTime, default=datetime.now, onupdate=datetime.now, nullable=True) + + __table_args__ = ( + sqla.Index('ti_user_id_changed_on', user_id, changed_on), + ) + + def to_dict(self): + return { + 'id': self.id, + 'database_id': self.database_id, + 'tab_name': self.tab_name, + 'user_id': self.user_id, + 'status': self.status, + 'schema': self.schema, + 'sql': self.sql, + 'limit': self.limit, + 'progress': self.progress, + 'error_message': self.error_message, + 'start_time': self.start_time, + 'end_time': self.end_time, + 'changed_on': self.end_time + } diff --git a/caravel/views.py b/caravel/views.py index 17db21c9b8b6..59219d2b4741 100755 --- a/caravel/views.py +++ b/caravel/views.py @@ -10,7 +10,7 @@ import sys import time import traceback -from datetime import datetime +from datetime import datetime, timedelta import functools import pandas as pd @@ -1525,34 +1525,43 @@ def sql_json(self): mimetype="application/json") @has_access - @expose("/queries/", methods=['GET']) + @expose("/queries/") @log_this - def queries(self): - """Runs arbitrary sql and returns and json""" - last_updated = request.form.get('timestamp') - s = db.session() - query = s.query(models.Query).filter_by(id=query_id).first() - mydb = s.query(models.Database).filter_by(id=query.database_id).first() + def queries(self, last_updated_ms): + """Get the updated queries.""" - if not (self.can_access( - 'all_datasource_access', 'all_datasource_access') or - self.can_access('database_access', mydb.perm)): - raise utils.CaravelSecurityException(_( - "SQL Lab requires the `all_datasource_access` or " - "specific DB permission")) + if not g.user.get_id(): + return Response( + json.dumps({'error': "Please login to access the queries."}), + status=403, + mimetype="application/json") + + # Unix time, milliseconds. + if not last_updated_ms: + last_updated_ms = 0 + + # Local date time, DO NOT USE IT. + # last_updated_dt = datetime.fromtimestamp(int(last_updated_ms) / 1000) + + # UTC date time, same that is stored in the DB. + last_updated_dt = utils.EPOCH + timedelta( + seconds=int(last_updated_ms) / 1000) - if query: + s = db.session() + sql_queries = s.query(models.Query).filter( + models.Query.user_id == g.user.get_id() or + models.Query.changed_on >= last_updated_dt) + + if sql_queries: + dict_queries = [q.to_dict() for q in sql_queries] return Response( - json.dumps({ - 'status': query.status, - 'progress': query.progress - }), + json.dumps(dict_queries, default=utils.json_int_dttm_ser), status=200, mimetype="application/json") return Response( json.dumps({ - 'error': "Query with id {} wasn't found".format(query_id), + 'error': "No updates for the user {}".format(g.user.get_id()), }), status=404, mimetype="application/json") diff --git a/tests/core_tests.py b/tests/core_tests.py index ed33c611e0f6..76ffb4e58cdd 100644 --- a/tests/core_tests.py +++ b/tests/core_tests.py @@ -357,8 +357,43 @@ def test_sql_json_has_access(self): appbuilder.sm.find_role('Astronaut'), password='general') data = self.run_sql('SELECT * FROM ab_user', 'gagarin') + db.session.query(models.Query).delete() + db.session.commit() assert len(data['data']) > 0 + def test_queries_endpoint(self): + db.session.query(models.Query).delete() + resp = self.client.get('/caravel/queries/{}'.format(0)) + self.assertEquals(403, resp.status_code) + + self.login('admin') + resp = self.client.get('/caravel/queries/{}'.format(0)) + data = json.loads(resp.data.decode('utf-8')) + self.assertEquals(0, len(data)) + self.logout() + + self.run_sql("SELECT * FROM ab_user", 'admin') + self.run_sql("SELECT * FROM ab_user1", 'admin') + self.login('admin') + resp = self.client.get('/caravel/queries/{}'.format(0)) + data = json.loads(resp.data.decode('utf-8')) + self.assertEquals(2, len(data)) + + query = db.session.query(models.Query).filter_by( + sql='SELECT * FROM ab_user').first() + query.changed_on = utils.EPOCH + db.session.commit() + + resp = self.client.get('/caravel/queries/{}'.format(123456)) + data = json.loads(resp.data.decode('utf-8')) + self.assertEquals(1, len(data)) + + self.logout() + resp = self.client.get('/caravel/queries/{}'.format(0)) + self.assertEquals(403, resp.status_code) + + db.session.query(models.Query).delete() + def test_sql_json(self): data = self.run_sql("SELECT * FROM ab_user", 'admin') assert len(data['data']) > 0