redash/handlers/dashboards.py (252 lines of code) (raw):

from flask import request, url_for from funcy import project, partial from flask_restful import abort from redash import models from redash.handlers.base import ( BaseResource, get_object_or_404, paginate, filter_by_tags, order_results as _order_results, ) from redash.permissions import ( can_modify, require_admin_or_owner, require_object_modify_permission, require_permission, ) from redash.security import csp_allows_embeding from redash.serializers import ( DashboardSerializer, public_dashboard, ) from sqlalchemy.orm.exc import StaleDataError # Ordering map for relationships order_map = { "name": "lowercase_name", "-name": "-lowercase_name", "created_at": "created_at", "-created_at": "-created_at", } order_results = partial( _order_results, default_order="-created_at", allowed_orders=order_map ) class DashboardListResource(BaseResource): @require_permission("list_dashboards") def get(self): """ Lists all accessible dashboards. :qparam number page_size: Number of queries to return per page :qparam number page: Page number to retrieve :qparam number order: Name of column to order by :qparam number q: Full text search term Responds with an array of :ref:`dashboard <dashboard-response-label>` objects. """ search_term = request.args.get("q") if search_term: results = models.Dashboard.search( self.current_org, self.current_user.group_ids, self.current_user.id, search_term, ) else: results = models.Dashboard.all( self.current_org, self.current_user.group_ids, self.current_user.id ) results = filter_by_tags(results, models.Dashboard.tags) # order results according to passed order parameter, # special-casing search queries where the database # provides an order by search rank ordered_results = order_results(results, fallback=not bool(search_term)) page = request.args.get("page", 1, type=int) page_size = request.args.get("page_size", 25, type=int) response = paginate( ordered_results, page=page, page_size=page_size, serializer=DashboardSerializer, ) if search_term: self.record_event( {"action": "search", "object_type": "dashboard", "term": search_term} ) else: self.record_event({"action": "list", "object_type": "dashboard"}) return response @require_permission("create_dashboard") def post(self): """ Creates a new dashboard. :<json string name: Dashboard name Responds with a :ref:`dashboard <dashboard-response-label>`. """ dashboard_properties = request.get_json(force=True) dashboard = models.Dashboard( name=dashboard_properties["name"], org=self.current_org, user=self.current_user, is_draft=True, layout="[]", ) models.db.session.add(dashboard) models.db.session.commit() return DashboardSerializer(dashboard).serialize() class MyDashboardsResource(BaseResource): @require_permission("list_dashboards") def get(self): """ Retrieve a list of dashboards created by the current user. :qparam number page_size: Number of dashboards to return per page :qparam number page: Page number to retrieve :qparam number order: Name of column to order by :qparam number search: Full text search term Responds with an array of :ref:`dashboard <dashboard-response-label>` objects. """ search_term = request.args.get("q", "") if search_term: results = models.Dashboard.search_by_user(search_term, self.current_user) else: results = models.Dashboard.by_user(self.current_user) results = filter_by_tags(results, models.Dashboard.tags) # order results according to passed order parameter, # special-casing search queries where the database # provides an order by search rank ordered_results = order_results(results, fallback=not bool(search_term)) page = request.args.get("page", 1, type=int) page_size = request.args.get("page_size", 25, type=int) return paginate( ordered_results, page, page_size, DashboardSerializer ) class DashboardResource(BaseResource): @require_permission("list_dashboards") def get(self, dashboard_id=None): """ Retrieves a dashboard. :qparam number id: Id 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 <widget-response-label>` data :>json object options: Dashboard options .. _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 """ if request.args.get("legacy") is not None: fn = models.Dashboard.get_by_slug_and_org else: fn = models.Dashboard.get_by_id_and_org dashboard = get_object_or_404(fn, dashboard_id, self.current_org) response = DashboardSerializer( dashboard, with_widgets=True, user=self.current_user ).serialize() api_key = models.ApiKey.get_by_object(dashboard) if api_key: response["public_url"] = url_for( "redash.public_dashboard", token=api_key.api_key, org_slug=self.current_org.slug, _external=True, ) response["api_key"] = api_key.api_key response["can_edit"] = can_modify(dashboard, self.current_user) self.record_event( {"action": "view", "object_id": dashboard.id, "object_type": "dashboard"} ) return response @require_permission("edit_dashboard") def post(self, dashboard_id): """ Modifies a dashboard. :qparam number id: Id of dashboard to retrieve. Responds with the updated :ref:`dashboard <dashboard-response-label>`. :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_id, self.current_org) require_object_modify_permission(dashboard, self.current_user) updates = project( dashboard_properties, ( "name", "layout", "version", "tags", "is_draft", "is_archived", "dashboard_filters_enabled", "options", ), ) # SQLAlchemy handles the case where a concurrent transaction beats us # to the update. But we still have to make sure that we're not starting # out behind. if "version" in updates and updates["version"] != dashboard.version: abort(409) updates["changed_by"] = self.current_user self.update_model(dashboard, updates) models.db.session.add(dashboard) try: models.db.session.commit() except StaleDataError: abort(409) result = DashboardSerializer( dashboard, with_widgets=True, user=self.current_user ).serialize() self.record_event( {"action": "edit", "object_id": dashboard.id, "object_type": "dashboard"} ) return result @require_permission("edit_dashboard") def delete(self, dashboard_id): """ Archives a dashboard. :qparam number id: Id of dashboard to retrieve. Responds with the archived :ref:`dashboard <dashboard-response-label>`. """ dashboard = models.Dashboard.get_by_id_and_org(dashboard_id, self.current_org) dashboard.is_archived = True dashboard.record_changes(changed_by=self.current_user) models.db.session.add(dashboard) d = DashboardSerializer( dashboard, with_widgets=True, user=self.current_user ).serialize() models.db.session.commit() self.record_event( {"action": "archive", "object_id": dashboard.id, "object_type": "dashboard"} ) return d class PublicDashboardResource(BaseResource): decorators = BaseResource.decorators + [csp_allows_embeding] 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 <public-widget-label>`, corresponding to the rows and columns the widgets are displayed in """ if self.current_org.get_setting("disable_public_urls"): abort(400, message="Public URLs are disabled.") 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 else: dashboard = self.current_user.object return public_dashboard(dashboard) 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) models.db.session.flush() models.db.session.commit() public_url = url_for( "redash.public_dashboard", token=api_key.api_key, org_slug=self.current_org.slug, _external=True, ) self.record_event( { "action": "activate_api_key", "object_id": dashboard.id, "object_type": "dashboard", } ) 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) if api_key: api_key.active = False models.db.session.add(api_key) models.db.session.commit() self.record_event( { "action": "deactivate_api_key", "object_id": dashboard.id, "object_type": "dashboard", } ) class DashboardTagsResource(BaseResource): @require_permission("list_dashboards") def get(self): """ Lists all accessible dashboards. """ tags = models.Dashboard.all_tags(self.current_org, self.current_user) return {"tags": [{"name": name, "count": count} for name, count in tags]} class DashboardFavoriteListResource(BaseResource): def get(self): search_term = request.args.get("q") if search_term: base_query = models.Dashboard.search( self.current_org, self.current_user.group_ids, self.current_user.id, search_term, ) favorites = models.Dashboard.favorites( self.current_user, base_query=base_query ) else: favorites = models.Dashboard.favorites(self.current_user) favorites = filter_by_tags(favorites, models.Dashboard.tags) # order results according to passed order parameter, # special-casing search queries where the database # provides an order by search rank favorites = order_results(favorites, fallback=not bool(search_term)) page = request.args.get("page", 1, type=int) page_size = request.args.get("page_size", 25, type=int) # TODO: we don't need to check for favorite status here response = paginate(favorites, page, page_size, DashboardSerializer) self.record_event( { "action": "load_favorites", "object_type": "dashboard", "params": { "q": search_term, "tags": request.args.getlist("tags"), "page": page, }, } ) return response