Skip to content

Commit

Permalink
Add schedule_until field to queries, to allow expiry (re #15)
Browse files Browse the repository at this point in the history
  • Loading branch information
Allen Short authored and jezdez committed Aug 16, 2018
1 parent 2c3149c commit c8e9f42
Show file tree
Hide file tree
Showing 8 changed files with 82 additions and 1 deletion.
4 changes: 4 additions & 0 deletions client/app/components/queries/schedule-dialog.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,8 @@ <h4 class="modal-title">Refresh Schedule</h4>
<query-time-picker refresh-type="$ctrl.refreshType" query="$ctrl.query" save-query="$ctrl.saveQuery"></query-time-picker>
</label>
</div>
<label>
Stop scheduling at date/time (format yyyy-MM-ddTHH:mm:ss, like 2016-12-28T14:57:00):
<schedule-until query="$ctrl.query" save-query="$ctrl.saveQuery"></schedule-until>
</label>
</div>
12 changes: 12 additions & 0 deletions client/app/components/queries/schedule-dialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,17 @@ function queryRefreshSelect(clientConfig, Policy) {
};
}

function scheduleUntil() {
return {
restrict: 'E',
scope: {
query: '=',
saveQuery: '=',
},
template: '<input type="datetime-local" step="1" class="form-control" ng-model="query.scheduleUntil" ng-change="saveQuery()">',
};
}

const ScheduleForm = {
controller() {
this.query = this.resolve.query;
Expand All @@ -125,5 +136,6 @@ const ScheduleForm = {
export default function init(ngModule) {
ngModule.directive('queryTimePicker', queryTimePicker);
ngModule.directive('queryRefreshSelect', queryRefreshSelect);
ngModule.directive('scheduleUntil', scheduleUntil);
ngModule.component('scheduleDialog', ScheduleForm);
}
4 changes: 4 additions & 0 deletions client/app/services/query.js
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,10 @@ function QueryResource(
.format('HH:mm');
};

Query.prototype.hasScheduleExpiry = function hasScheduleExpiry() {
return (this.schedule && this.schedule_until);
};

Query.prototype.hasResult = function hasResult() {
return !!(this.latest_query_data || this.latest_query_data_id);
};
Expand Down
27 changes: 27 additions & 0 deletions migrations/versions/eb2f788f997e_.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""Add 'schedule_until' column to queries.
Revision ID: eb2f788f997e
Revises: d1eae8b9893e
Create Date: 2017-03-02 12:20:00.029066
"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = 'eb2f788f997e'
down_revision = 'd1eae8b9893e'
branch_labels = None
depends_on = None


def upgrade():
op.add_column(
'queries',
sa.Column('schedule_until', sa.DateTime(timezone=True), nullable=True))


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('queries', 'schedule_until')
2 changes: 2 additions & 0 deletions redash/handlers/queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ def post(self):
:<json string name:
:<json string description:
:<json string schedule: Schedule interval, in seconds, for repeated execution of this query
:<json string schedule_until: Time in ISO format to stop scheduling this query (may be null to run indefinitely)
:<json object options: Query options
.. _query-response-label:
Expand All @@ -114,6 +115,7 @@ def post(self):
:>json string query: Query text
:>json string query_hash: Hash of query text
:>json string schedule: Schedule interval, in seconds, for repeated execution of this query
:<json string schedule_until: Time in ISO format to stop scheduling this query (may be null to run indefinitely)
:>json string api_key: Key for public access to this query's results.
:>json boolean is_archived: Whether this query is displayed in indexes and search results or not.
:>json boolean is_draft: Whether this query is a draft or not
Expand Down
5 changes: 4 additions & 1 deletion redash/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -881,6 +881,7 @@ class Query(ChangeTrackingMixin, TimestampMixin, BelongsToOrgMixin, db.Model):
is_draft = Column(db.Boolean, default=True, index=True)
schedule = Column(db.String(10), nullable=True)
schedule_failures = Column(db.Integer, default=0)
schedule_until = Column(db.DateTime(True), nullable=True)
visualizations = db.relationship("Visualization", cascade="all, delete-orphan")
options = Column(MutableDict.as_mutable(PseudoJSON), default={})
search_vector = Column(TSVectorType('id', 'name', 'description', 'query',
Expand Down Expand Up @@ -1001,7 +1002,9 @@ def by_user(cls, user):
def outdated_queries(cls):
queries = (db.session.query(Query)
.options(joinedload(Query.latest_query_data).load_only('retrieved_at'))
.filter(Query.schedule != None)
.filter(Query.schedule != None,
(Query.schedule_until == None) |
(Query.schedule_until > db.func.now()))
.order_by(Query.id))

now = utils.utcnow()
Expand Down
1 change: 1 addition & 0 deletions redash/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ def serialize_query(query, with_stats=False, with_visualizations=False, with_use
'query': query.query_text,
'query_hash': query.query_hash,
'schedule': query.schedule,
'schedule_until': query.schedule_until,
'api_key': query.api_key,
'is_archived': query.is_archived,
'is_draft': query.is_draft,
Expand Down
28 changes: 28 additions & 0 deletions tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,34 @@ def test_failure_extends_schedule(self):
query_result.retrieved_at = utcnow() - datetime.timedelta(minutes=17)
self.assertEqual(list(models.Query.outdated_queries()), [query])

def test_schedule_until_after(self):
"""
Queries with non-null ``schedule_until`` are not reported by
Query.outdated_queries() after the given time is past.
"""
three_hours_ago = utcnow() - datetime.timedelta(hours=3)
two_hours_ago = utcnow() - datetime.timedelta(hours=2)
query = self.factory.create_query(schedule="3600", schedule_until=three_hours_ago)
query_result = self.factory.create_query_result(query=query.query_text, retrieved_at=two_hours_ago)
query.latest_query_data = query_result

queries = models.Query.outdated_queries()
self.assertNotIn(query, queries)

def test_schedule_until_before(self):
"""
Queries with non-null ``schedule_until`` are reported by
Query.outdated_queries() before the given time is past.
"""
one_hour_from_now = utcnow() + datetime.timedelta(hours=1)
two_hours_ago = utcnow() - datetime.timedelta(hours=2)
query = self.factory.create_query(schedule="3600", schedule_until=one_hour_from_now)
query_result = self.factory.create_query_result(query=query.query_text, retrieved_at=two_hours_ago)
query.latest_query_data = query_result

queries = models.Query.outdated_queries()
self.assertIn(query, queries)


class QueryArchiveTest(BaseTestCase):
def setUp(self):
Expand Down

0 comments on commit c8e9f42

Please sign in to comment.