diff --git a/migrations/0025_add_notification_destination.py b/migrations/0025_add_notification_destination.py
new file mode 100644
index 0000000000..4d46f6f5e0
--- /dev/null
+++ b/migrations/0025_add_notification_destination.py
@@ -0,0 +1,8 @@
+from redash.models import db, QuerySnippet
+
+if __name__ == '__main__':
+ with db.database.transaction():
+ if not QuerySnippet.table_exists():
+ QuerySnippet.create_table()
+
+ db.close_db(None)
diff --git a/rd_ui/app/app_layout.html b/rd_ui/app/app_layout.html
index 70e1c7e1cc..2b5e142965 100644
--- a/rd_ui/app/app_layout.html
+++ b/rd_ui/app/app_layout.html
@@ -78,6 +78,7 @@
diff --git a/rd_ui/app/views/query_snippets/list.html b/rd_ui/app/views/query_snippets/list.html
new file mode 100644
index 0000000000..e0e7b5ac9f
--- /dev/null
+++ b/rd_ui/app/views/query_snippets/list.html
@@ -0,0 +1,13 @@
+
+
+
diff --git a/rd_ui/app/views/query_snippets/show.html b/rd_ui/app/views/query_snippets/show.html
new file mode 100644
index 0000000000..2dfbf9c43d
--- /dev/null
+++ b/rd_ui/app/views/query_snippets/show.html
@@ -0,0 +1,36 @@
+
+
+
+
+
diff --git a/redash/handlers/api.py b/redash/handlers/api.py
index 16fa2e59b2..5372e69b89 100644
--- a/redash/handlers/api.py
+++ b/redash/handlers/api.py
@@ -17,6 +17,7 @@
from redash.handlers.groups import GroupListResource, GroupResource, GroupMemberListResource, GroupMemberResource, \
GroupDataSourceListResource, GroupDataSourceResource
from redash.handlers.destinations import DestinationTypeListResource, DestinationResource, DestinationListResource
+from redash.handlers.query_snippets import QuerySnippetListResource, QuerySnippetResource
class ApiExt(Api):
@@ -90,3 +91,6 @@ def json_representation(data, code, headers=None):
api.add_org_resource(DestinationTypeListResource, '/api/destinations/types', endpoint='destination_types')
api.add_org_resource(DestinationResource, '/api/destinations/
', endpoint='destination')
api.add_org_resource(DestinationListResource, '/api/destinations', endpoint='destinations')
+
+api.add_org_resource(QuerySnippetResource, '/api/query_snippets/', endpoint='query_snippet')
+api.add_org_resource(QuerySnippetListResource, '/api/query_snippets', endpoint='query_snippets')
diff --git a/redash/handlers/query_snippets.py b/redash/handlers/query_snippets.py
new file mode 100644
index 0000000000..7614bce685
--- /dev/null
+++ b/redash/handlers/query_snippets.py
@@ -0,0 +1,64 @@
+from flask import request
+from funcy import project
+
+from redash import models
+from redash.permissions import require_admin_or_owner
+from redash.handlers.base import BaseResource, require_fields, get_object_or_404
+
+
+class QuerySnippetResource(BaseResource):
+ def get(self, snippet_id):
+ snippet = get_object_or_404(models.QuerySnippet.get_by_id_and_org, snippet_id, self.current_org)
+ return snippet.to_dict()
+
+ def post(self, snippet_id):
+ req = request.get_json(True)
+ params = project(req, ('trigger', 'description', 'snippet'))
+ snippet = get_object_or_404(models.QuerySnippet.get_by_id_and_org, snippet_id, self.current_org)
+ require_admin_or_owner(snippet.user.id)
+
+ snippet.update_instance(**params)
+
+ self.record_event({
+ 'action': 'edit',
+ 'object_id': snippet.id,
+ 'object_type': 'query_snippet'
+ })
+
+ return snippet.to_dict()
+
+ def delete(self, snippet_id):
+ snippet = get_object_or_404(models.QuerySnippet.get_by_id_and_org, snippet_id, self.current_org)
+ require_admin_or_owner(snippet.user.id)
+ snippet.delete_instance()
+
+ self.record_event({
+ 'action': 'delete',
+ 'object_id': snippet.id,
+ 'object_type': 'query_snippet'
+ })
+
+
+class QuerySnippetListResource(BaseResource):
+ def post(self):
+ req = request.get_json(True)
+ require_fields(req, ('trigger', 'description', 'snippet'))
+
+ snippet = models.QuerySnippet.create(
+ trigger=req['trigger'],
+ description=req['description'],
+ snippet=req['snippet'],
+ user=self.current_user,
+ org=self.current_org
+ )
+
+ self.record_event({
+ 'action': 'create',
+ 'object_id': snippet.id,
+ 'object_type': 'query_snippet'
+ })
+
+ return snippet.to_dict()
+
+ def get(self):
+ return [snippet.to_dict() for snippet in models.QuerySnippet.all(org=self.current_org)]
diff --git a/redash/handlers/static.py b/redash/handlers/static.py
index 7fc1d6ad64..7706b27337 100644
--- a/redash/handlers/static.py
+++ b/redash/handlers/static.py
@@ -81,6 +81,8 @@ def register_static_routes(rules):
'/users/',
'/destinations',
'/destinations/',
+ '/query_snippets',
+ '/query_snippets/',
'/groups',
'/groups/',
'/groups//data_sources',
diff --git a/redash/models.py b/redash/models.py
index 68ecc3e78c..c9ca984b9a 100644
--- a/redash/models.py
+++ b/redash/models.py
@@ -1196,6 +1196,35 @@ def notify(self, alert, query, user, new_state, app, host):
return destination.notify(alert, query, user, new_state, app, host, options)
+class QuerySnippet(ModelTimestampsMixin, BaseModel, BelongsToOrgMixin):
+ id = peewee.PrimaryKeyField()
+ org = peewee.ForeignKeyField(Organization, related_name="query_snippets")
+ trigger = peewee.CharField(unique=True)
+ description = peewee.TextField()
+ user = peewee.ForeignKeyField(User, related_name="query_snippets")
+ snippet = peewee.TextField()
+
+ class Meta:
+ db_table = 'query_snippets'
+
+ @classmethod
+ def all(cls, org):
+ return cls.select().where(cls.org==org)
+
+ def to_dict(self):
+ d = {
+ 'id': self.id,
+ 'trigger': self.trigger,
+ 'description': self.description,
+ 'snippet': self.snippet,
+ 'user': self.user.to_dict(),
+ 'updated_at': self.updated_at,
+ 'created_at': self.created_at
+ }
+
+ return d
+
+
all_models = (Organization, Group, DataSource, DataSourceGroup, User, QueryResult, Query, Alert, Dashboard, Visualization, Widget, Event, NotificationDestination, AlertSubscription, ApiKey)