diff --git a/redash/__init__.py b/redash/__init__.py index 621c1c7c8b..2d8c0bc987 100644 --- a/redash/__init__.py +++ b/redash/__init__.py @@ -82,7 +82,7 @@ def to_url(self, value): return value -def create_app(): +def create_app(load_admin=True): from redash import handlers from redash.admin import init_admin from redash.models import db @@ -118,10 +118,11 @@ def create_app(): provision_app(app) db.init_app(app) migrate.init_app(app, db) - init_admin(app) + if load_admin: + init_admin(app) mail.init_app(app) setup_authentication(app) - handlers.init_app(app) limiter.init_app(app) + handlers.init_app(app) return app diff --git a/redash/handlers/dashboards.py b/redash/handlers/dashboards.py index e29b81a7e8..ce074b7056 100644 --- a/redash/handlers/dashboards.py +++ b/redash/handlers/dashboards.py @@ -15,6 +15,9 @@ class RecentDashboardsResource(BaseResource): @require_permission('list_dashboards') def get(self): + """ + Lists dashboards modified in the last 7 days. + """ recent = [d.to_dict() for d in models.Dashboard.recent(self.current_org, self.current_user.group_ids, self.current_user.id, for_user=True)] global_recent = [] @@ -27,11 +30,21 @@ def get(self): class DashboardListResource(BaseResource): @require_permission('list_dashboards') def get(self): + """ + Lists all accessible dashboards. + """ results = models.Dashboard.all(self.current_org, self.current_user.group_ids, self.current_user.id) return [q.to_dict() for q in results] @require_permission('create_dashboard') def post(self): + """ + Creates a new dashboard. + + :`. + """ dashboard_properties = request.get_json(force=True) dashboard = models.Dashboard(name=dashboard_properties['name'], org=self.current_org, @@ -46,6 +59,39 @@ def post(self): class DashboardResource(BaseResource): @require_permission('list_dashboards') def get(self, dashboard_slug=None): + """ + Retrieves a dashboard. + + :qparam string slug: Slug of dashboard to retrieve. + + .. _dashboard-response-label: + + :>json number id: Dashboard ID + :>json string name: + :>json string slug: + :>json number user_id: ID of the dashboard creator + :>json string created_at: ISO format timestamp for dashboard creation + :>json string updated_at: ISO format timestamp for last dashboard modification + :>json number version: Revision number of dashboard + :>json boolean dashboard_filters_enabled: Whether filters are enabled or not + :>json boolean is_archived: Whether this dashboard has been removed from the index or not + :>json boolean is_draft: Whether this dashboard is a draft or not. + :>json array layout: Array of arrays containing widget IDs, corresponding to the rows and columns the widgets are displayed in + :>json array widgets: Array of arrays containing :ref:`widget ` data + + .. _widget-response-label: + + Widget structure: + + :>json number widget.id: Widget ID + :>json number widget.width: Widget size + :>json object widget.options: Widget options + :>json number widget.dashboard_id: ID of dashboard containing this widget + :>json string widget.text: Widget contents, if this is a text-box widget + :>json object widget.visualization: Widget contents, if this is a visualization widget + :>json string widget.created_at: ISO format timestamp for widget creation + :>json string widget.updated_at: ISO format timestamp for last widget modification + """ dashboard = get_object_or_404(models.Dashboard.get_by_slug_and_org, dashboard_slug, self.current_org) response = dashboard.to_dict(with_widgets=True, user=self.current_user) @@ -60,6 +106,16 @@ def get(self, dashboard_slug=None): @require_permission('edit_dashboard') def post(self, dashboard_slug): + """ + Modifies a dashboard. + + :qparam string slug: Slug of dashboard to retrieve. + + Responds with the updated :ref:`dashboard `. + + :status 200: success + :status 409: Version conflict -- dashboard modified since last read + """ dashboard_properties = request.get_json(force=True) # TODO: either convert all requests to use slugs or ids dashboard = models.Dashboard.get_by_id_and_org(dashboard_slug, self.current_org) @@ -89,6 +145,13 @@ def post(self, dashboard_slug): @require_permission('edit_dashboard') def delete(self, dashboard_slug): + """ + Archives a dashboard. + + :qparam string slug: Slug of dashboard to retrieve. + + Responds with the archived :ref:`dashboard `. + """ dashboard = models.Dashboard.get_by_slug_and_org(dashboard_slug, self.current_org) dashboard.is_archived = True dashboard.record_changes(changed_by=self.current_user) @@ -100,6 +163,12 @@ def delete(self, dashboard_slug): class PublicDashboardResource(BaseResource): def get(self, token): + """ + Retrieve a public dashboard. + + :param token: An API key for a public dashboard. + :>json array widgets: An array of arrays of :ref:`public widgets `, corresponding to the rows and columns the widgets are displayed in + """ if not isinstance(self.current_user, models.ApiUser): api_key = get_object_or_404(models.ApiKey.get_by_api_key, token) dashboard = api_key.object @@ -111,6 +180,13 @@ def get(self, token): class DashboardShareResource(BaseResource): def post(self, dashboard_id): + """ + Allow anonymous access to a dashboard. + + :param dashboard_id: The numeric ID of the dashboard to share. + :>json string public_url: The URL for anonymous access to the dashboard. + :>json api_key: The API key to use when accessing it. + """ dashboard = models.Dashboard.get_by_id_and_org(dashboard_id, self.current_org) require_admin_or_owner(dashboard.user_id) api_key = models.ApiKey.create_for_object(dashboard, self.current_user) @@ -128,6 +204,11 @@ def post(self, dashboard_id): return {'public_url': public_url, 'api_key': api_key.api_key} def delete(self, dashboard_id): + """ + Disable anonymous access to a dashboard. + + :param dashboard_id: The numeric ID of the dashboard to unshare. + """ dashboard = models.Dashboard.get_by_id_and_org(dashboard_id, self.current_org) require_admin_or_owner(dashboard.user_id) api_key = models.ApiKey.get_by_object(dashboard) diff --git a/redash/handlers/queries.py b/redash/handlers/queries.py index 3d22382ba7..1d4afec0c0 100644 --- a/redash/handlers/queries.py +++ b/redash/handlers/queries.py @@ -21,6 +21,12 @@ @routes.route(org_scoped_rule('/api/queries/format'), methods=['POST']) @login_required def format_sql_query(org_slug=None): + """ + Formats an SQL query using the Python ``sqlparse`` formatter. + + :json string query: Formatted SQL text + """ arguments = request.get_json(force=True) query = arguments.get("query", "") @@ -30,6 +36,13 @@ def format_sql_query(org_slug=None): class QuerySearchResource(BaseResource): @require_permission('view_query') def get(self): + """ + Search query text, titles, and descriptions. + + :qparam string q: Search term + + Responds with a list of :ref:`query ` objects. + """ term = request.args.get('q', '') include_drafts = request.args.get('include_drafts') is not None @@ -39,6 +52,11 @@ def get(self): class QueryRecentResource(BaseResource): @require_permission('view_query') def get(self): + """ + Retrieve up to 20 queries modified in the last 7 days. + + Responds with a list of :ref:`query ` objects. + """ queries = models.Query.recent(self.current_user.group_ids, self.current_user.id) recent = [d.to_dict(with_last_modified_by=False) for d in queries] @@ -52,6 +70,38 @@ def get(self): class QueryListResource(BaseResource): @require_permission('create_query') def post(self): + """ + Create a new query. + + :json number id: Query ID + :>json number latest_query_data_id: ID for latest output data from this query + :>json string name: + :>json string description: + :>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 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 + :>json string updated_at: Time of last modification, in ISO format + :>json string created_at: Time of creation, in ISO format + :>json number data_source_id: ID of the data source this query will run on + :>json object options: Query options + :>json number version: Revision version (for update conflict avoidance) + :>json number user_id: ID of query creator + :>json number last_modified_by_id: ID of user who last modified this query + :>json string retrieved_at: Time when query results were last retrieved, in ISO format (may be null) + :>json number runtime: Runtime of last query execution, in seconds (may be null) + """ query_def = request.get_json(force=True) data_source = models.DataSource.get_by_id_and_org(query_def.pop('data_source_id'), self.current_org) require_access(data_source.groups, self.current_user, not_view_only) @@ -78,6 +128,15 @@ def post(self): @require_permission('view_query') def get(self): + """ + Retrieve a list of queries. + + :qparam number page_size: Number of queries to return + :qparam number page: Page number to retrieve + + Responds with an array of :ref:`query ` objects. + """ + results = models.Query.all_queries(self.current_user.group_ids, self.current_user.id) page = request.args.get('page', 1, type=int) page_size = request.args.get('page_size', 25, type=int) @@ -87,6 +146,15 @@ def get(self): class MyQueriesResource(BaseResource): @require_permission('view_query') def get(self): + """ + Retrieve a list of queries created by the current user. + + :qparam number page_size: Number of queries to return + :qparam number page: Page number to retrieve + + Responds with an array of :ref:`query ` objects. + """ + drafts = request.args.get('drafts') is not None results = models.Query.by_user(self.current_user) page = request.args.get('page', 1, type=int) page_size = request.args.get('page_size', 25, type=int) @@ -96,6 +164,19 @@ def get(self): class QueryResource(BaseResource): @require_permission('edit_query') def post(self, query_id): + """ + Modify a query. + + :param query_id: ID of query to update + :` object. + """ query = get_object_or_404(models.Query.get_by_id_and_org, query_id, self.current_org) query_def = request.get_json(force=True) @@ -125,6 +206,13 @@ def post(self, query_id): @require_permission('view_query') def get(self, query_id): + """ + Retrieve a query. + + :param query_id: ID of query to fetch + + Responds with the :ref:`query ` contents. + """ q = get_object_or_404(models.Query.get_by_id_and_org, query_id, self.current_org) require_access(q.groups, self.current_user, view_only) @@ -134,6 +222,11 @@ def get(self, query_id): # TODO: move to resource of its own? (POST /queries/{id}/archive) def delete(self, query_id): + """ + Archives a query. + + :param query_id: ID of query to archive + """ query = get_object_or_404(models.Query.get_by_id_and_org, query_id, self.current_org) require_admin_or_owner(query.user_id) query.archive(self.current_user) @@ -143,6 +236,13 @@ def delete(self, query_id): class QueryForkResource(BaseResource): @require_permission('edit_query') def post(self, query_id): + """ + Creates a new query, copying the query text from an existing one. + + :param query_id: ID of query to fork + + Responds with created :ref:`query ` object. + """ query = get_object_or_404(models.Query.get_by_id_and_org, query_id, self.current_org) forked_query = query.fork(self.current_user) models.db.session.commit() @@ -151,6 +251,13 @@ def post(self, query_id): class QueryRefreshResource(BaseResource): def post(self, query_id): + """ + Execute a query, updating the query object with the results. + + :param query_id: ID of query to execute + + Responds with query task details. + """ query = get_object_or_404(models.Query.get_by_id_and_org, query_id, self.current_org) require_access(query.groups, self.current_user, not_view_only) diff --git a/redash/handlers/query_results.py b/redash/handlers/query_results.py index d7ca759df3..81510ae387 100644 --- a/redash/handlers/query_results.py +++ b/redash/handlers/query_results.py @@ -52,6 +52,14 @@ def run_query(data_source, parameter_values, query_text, query_id, max_age=0): class QueryResultListResource(BaseResource): @require_permission('execute_query') def post(self): + """ + Execute a query (or retrieve recent results). + + :qparam string query: The query text to execute + :qparam number query_id: The query object to update with the result (optional) + :qparam number max_age: If query results less than `max_age` seconds old are available, return them, otherwise execute the query; if omitted, always execute + :qparam number data_source_id: ID of data source to query + """ params = request.get_json(force=True) parameter_values = collect_parameters_from_request(request.args) @@ -102,6 +110,21 @@ def options(self, query_id=None, query_result_id=None, filetype='json'): @require_permission('view_query') def get(self, query_id=None, query_result_id=None, filetype='json'): + """ + Retrieve query results. + + :param number query_id: The ID of the query whose results should be fetched + :param number query_result_id: the ID of the query result to fetch + :param string filetype: Format to return. One of 'json', 'xlsx', or 'csv'. Defaults to 'json'. + + :json object widget: The created widget + :>json array layout: The new layout of the dashboard this widget was added to + :>json boolean new_row: Whether this widget was added on a new row or not + :>json number version: The revision number of the dashboard + """ widget_properties = request.get_json(force=True) dashboard = models.Dashboard.get_by_id_and_org(widget_properties.pop('dashboard_id'), self.current_org) require_object_modify_permission(dashboard, self.current_user) @@ -56,7 +70,14 @@ def post(self): class WidgetResource(BaseResource): @require_permission('edit_dashboard') def post(self, widget_id): - # This method currently handles Text Box widgets only. + """ + Updates a widget in a dashboard. + This method currently handles Text Box widgets only. + + :param number widget_id: The ID of the widget to modify + + :json array layout: New layout of dashboard this widget was removed from + :>json number version: Revision number of dashboard + """ widget = models.Widget.get_by_id_and_org(widget_id, self.current_org) require_object_modify_permission(widget.dashboard, self.current_user) widget.delete()