[dashboard] feat: REST API (#8694)
This commit is contained in:
parent
1719c4b5a3
commit
2f2ac00a09
|
|
@ -497,6 +497,7 @@ class Dashboard( # pylint: disable=too-many-instance-attributes
|
|||
meta = MetaData(bind=self.get_sqla_engine())
|
||||
meta.reflect()
|
||||
|
||||
@renders("dashboard_title")
|
||||
def dashboard_link(self) -> Markup:
|
||||
title = escape(self.dashboard_title or "<empty>")
|
||||
return Markup(f'<a href="{self.url}">{title}</a>')
|
||||
|
|
|
|||
|
|
@ -536,7 +536,8 @@ def validate_json(obj):
|
|||
if obj:
|
||||
try:
|
||||
json.loads(obj)
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
logging.error(f"JSON is not valid {e}")
|
||||
raise SupersetException("JSON is not valid")
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ from superset.legacy import update_time_range
|
|||
from superset.utils import core as utils
|
||||
|
||||
from .base import api, BaseSupersetView, handle_api_exception
|
||||
from .dashboard import api as dashboard_api # pylint: disable=unused-import
|
||||
from .database import api as database_api # pylint: disable=unused-import
|
||||
|
||||
|
||||
class Api(BaseSupersetView):
|
||||
|
|
|
|||
|
|
@ -23,13 +23,14 @@ from typing import Any, Dict, Optional
|
|||
import simplejson as json
|
||||
import yaml
|
||||
from flask import abort, flash, g, get_flashed_messages, redirect, Response, session
|
||||
from flask_appbuilder import BaseView, ModelView
|
||||
from flask_appbuilder import BaseView, Model, ModelView
|
||||
from flask_appbuilder.actions import action
|
||||
from flask_appbuilder.forms import DynamicForm
|
||||
from flask_appbuilder.models.sqla.filters import BaseFilter
|
||||
from flask_appbuilder.widgets import ListWidget
|
||||
from flask_babel import get_locale, gettext as __, lazy_gettext as _
|
||||
from flask_wtf.form import FlaskForm
|
||||
from marshmallow import Schema
|
||||
from sqlalchemy import or_
|
||||
from werkzeug.exceptions import HTTPException
|
||||
from wtforms.fields.core import Field, UnboundField
|
||||
|
|
@ -352,6 +353,24 @@ class DatasourceFilter(BaseFilter): # pylint: disable=too-few-public-methods
|
|||
)
|
||||
|
||||
|
||||
class BaseSupersetSchema(Schema):
|
||||
"""
|
||||
Extends Marshmallow schema so that we can pass a Model to load
|
||||
(following marshamallow-sqlalchemy pattern). This is useful
|
||||
to perform partial model merges on HTTP PUT
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.instance = None
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def load(
|
||||
self, data, many=None, partial=None, instance: Model = None, **kwargs
|
||||
): # pylint: disable=arguments-differ
|
||||
self.instance = instance
|
||||
return super().load(data, many=many, partial=partial, **kwargs)
|
||||
|
||||
|
||||
class CsvResponse(Response): # pylint: disable=too-many-ancestors
|
||||
"""
|
||||
Override Response to take into account csv encoding from config.py
|
||||
|
|
|
|||
|
|
@ -40,7 +40,6 @@ from flask import (
|
|||
url_for,
|
||||
)
|
||||
from flask_appbuilder import expose
|
||||
from flask_appbuilder.actions import action
|
||||
from flask_appbuilder.models.sqla.interface import SQLAInterface
|
||||
from flask_appbuilder.security.decorators import has_access, has_access_api
|
||||
from flask_appbuilder.security.sqla import models as ab_models
|
||||
|
|
@ -103,7 +102,9 @@ from .base import (
|
|||
json_success,
|
||||
SupersetModelView,
|
||||
)
|
||||
from .database import api as database_api, views as in_views
|
||||
from .dashboard import views as dash_views
|
||||
from .dashboard.filters import DashboardFilter
|
||||
from .database import views as in_views
|
||||
from .utils import (
|
||||
apply_display_max_row_limit,
|
||||
bootstrap_user_data,
|
||||
|
|
@ -255,69 +256,6 @@ class SliceFilter(BaseFilter):
|
|||
)
|
||||
|
||||
|
||||
class DashboardFilter(BaseFilter):
|
||||
"""
|
||||
List dashboards with the following criteria:
|
||||
1. Those which the user owns
|
||||
2. Those which the user has favorited
|
||||
3. Those which have been published (if they have access to at least one slice)
|
||||
|
||||
If the user is an admin show them all dashboards.
|
||||
This means they do not get curation but can still sort by "published"
|
||||
if they wish to see those dashboards which are published first
|
||||
"""
|
||||
|
||||
def apply(self, query, func):
|
||||
Dash = models.Dashboard
|
||||
User = ab_models.User
|
||||
Slice = models.Slice
|
||||
Favorites = models.FavStar
|
||||
|
||||
user_roles = [role.name.lower() for role in list(get_user_roles())]
|
||||
if "admin" in user_roles:
|
||||
return query
|
||||
|
||||
datasource_perms = security_manager.user_view_menu_names("datasource_access")
|
||||
schema_perms = security_manager.user_view_menu_names("schema_access")
|
||||
all_datasource_access = security_manager.all_datasource_access()
|
||||
published_dash_query = (
|
||||
db.session.query(Dash.id)
|
||||
.join(Dash.slices)
|
||||
.filter(
|
||||
and_(
|
||||
Dash.published == True, # noqa
|
||||
or_(
|
||||
Slice.perm.in_(datasource_perms),
|
||||
Slice.schema_perm.in_(schema_perms),
|
||||
all_datasource_access,
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
users_favorite_dash_query = db.session.query(Favorites.obj_id).filter(
|
||||
and_(
|
||||
Favorites.user_id == User.get_user_id(),
|
||||
Favorites.class_name == "Dashboard",
|
||||
)
|
||||
)
|
||||
owner_ids_query = (
|
||||
db.session.query(Dash.id)
|
||||
.join(Dash.owners)
|
||||
.filter(User.id == User.get_user_id())
|
||||
)
|
||||
|
||||
query = query.filter(
|
||||
or_(
|
||||
Dash.id.in_(owner_ids_query),
|
||||
Dash.id.in_(published_dash_query),
|
||||
Dash.id.in_(users_favorite_dash_query),
|
||||
)
|
||||
)
|
||||
|
||||
return query
|
||||
|
||||
|
||||
if config["ENABLE_ACCESS_REQUEST"]:
|
||||
|
||||
class AccessRequestsModelView(SupersetModelView, DeleteMixin):
|
||||
|
|
@ -495,116 +433,8 @@ class SliceAddView(SliceModelView):
|
|||
appbuilder.add_view_no_menu(SliceAddView)
|
||||
|
||||
|
||||
class DashboardModelView(SupersetModelView, DeleteMixin):
|
||||
route_base = "/dashboard"
|
||||
datamodel = SQLAInterface(models.Dashboard)
|
||||
|
||||
list_title = _("Dashboards")
|
||||
show_title = _("Show Dashboard")
|
||||
add_title = _("Add Dashboard")
|
||||
edit_title = _("Edit Dashboard")
|
||||
|
||||
list_columns = ["dashboard_link", "creator", "published", "modified"]
|
||||
order_columns = ["modified", "published"]
|
||||
edit_columns = [
|
||||
"dashboard_title",
|
||||
"slug",
|
||||
"owners",
|
||||
"position_json",
|
||||
"css",
|
||||
"json_metadata",
|
||||
"published",
|
||||
]
|
||||
show_columns = edit_columns + ["table_names", "charts"]
|
||||
search_columns = ("dashboard_title", "slug", "owners", "published")
|
||||
add_columns = edit_columns
|
||||
base_order = ("changed_on", "desc")
|
||||
description_columns = {
|
||||
"position_json": _(
|
||||
"This json object describes the positioning of the widgets in "
|
||||
"the dashboard. It is dynamically generated when adjusting "
|
||||
"the widgets size and positions by using drag & drop in "
|
||||
"the dashboard view"
|
||||
),
|
||||
"css": _(
|
||||
"The CSS for individual dashboards can be altered here, or "
|
||||
"in the dashboard view where changes are immediately "
|
||||
"visible"
|
||||
),
|
||||
"slug": _("To get a readable URL for your dashboard"),
|
||||
"json_metadata": _(
|
||||
"This JSON object is generated dynamically when clicking "
|
||||
"the save or overwrite button in the dashboard view. It "
|
||||
"is exposed here for reference and for power users who may "
|
||||
"want to alter specific parameters."
|
||||
),
|
||||
"owners": _("Owners is a list of users who can alter the dashboard."),
|
||||
"published": _(
|
||||
"Determines whether or not this dashboard is "
|
||||
"visible in the list of all dashboards"
|
||||
),
|
||||
}
|
||||
base_filters = [["slice", DashboardFilter, lambda: []]]
|
||||
label_columns = {
|
||||
"dashboard_link": _("Dashboard"),
|
||||
"dashboard_title": _("Title"),
|
||||
"slug": _("Slug"),
|
||||
"charts": _("Charts"),
|
||||
"owners": _("Owners"),
|
||||
"creator": _("Creator"),
|
||||
"modified": _("Modified"),
|
||||
"position_json": _("Position JSON"),
|
||||
"css": _("CSS"),
|
||||
"json_metadata": _("JSON Metadata"),
|
||||
"table_names": _("Underlying Tables"),
|
||||
}
|
||||
|
||||
def pre_add(self, obj):
|
||||
obj.slug = obj.slug or None
|
||||
if obj.slug:
|
||||
obj.slug = obj.slug.strip()
|
||||
obj.slug = obj.slug.replace(" ", "-")
|
||||
obj.slug = re.sub(r"[^\w\-]+", "", obj.slug)
|
||||
if g.user not in obj.owners:
|
||||
obj.owners.append(g.user)
|
||||
utils.validate_json(obj.json_metadata)
|
||||
utils.validate_json(obj.position_json)
|
||||
owners = [o for o in obj.owners]
|
||||
for slc in obj.slices:
|
||||
slc.owners = list(set(owners) | set(slc.owners))
|
||||
|
||||
def pre_update(self, obj):
|
||||
check_ownership(obj)
|
||||
self.pre_add(obj)
|
||||
|
||||
def pre_delete(self, obj):
|
||||
check_ownership(obj)
|
||||
|
||||
@action("mulexport", __("Export"), __("Export dashboards?"), "fa-database")
|
||||
def mulexport(self, items):
|
||||
if not isinstance(items, list):
|
||||
items = [items]
|
||||
ids = "".join("&id={}".format(d.id) for d in items)
|
||||
return redirect("/dashboard/export_dashboards_form?{}".format(ids[1:]))
|
||||
|
||||
@event_logger.log_this
|
||||
@has_access
|
||||
@expose("/export_dashboards_form")
|
||||
def download_dashboards(self):
|
||||
if request.args.get("action") == "go":
|
||||
ids = request.args.getlist("id")
|
||||
return Response(
|
||||
models.Dashboard.export_dashboards(ids),
|
||||
headers=generate_download_headers("json"),
|
||||
mimetype="application/text",
|
||||
)
|
||||
return self.render_template(
|
||||
"superset/export_dashboards.html", dashboards_url="/dashboard/list"
|
||||
)
|
||||
|
||||
|
||||
appbuilder.add_view(
|
||||
DashboardModelView,
|
||||
dash_views.DashboardModelView,
|
||||
"Dashboards",
|
||||
label=__("Dashboards"),
|
||||
icon="fa-dashboard",
|
||||
|
|
@ -613,47 +443,6 @@ appbuilder.add_view(
|
|||
)
|
||||
|
||||
|
||||
class DashboardModelViewAsync(DashboardModelView):
|
||||
route_base = "/dashboardasync"
|
||||
list_columns = [
|
||||
"id",
|
||||
"dashboard_link",
|
||||
"creator",
|
||||
"modified",
|
||||
"dashboard_title",
|
||||
"changed_on",
|
||||
"url",
|
||||
"changed_by_name",
|
||||
]
|
||||
label_columns = {
|
||||
"dashboard_link": _("Dashboard"),
|
||||
"dashboard_title": _("Title"),
|
||||
"creator": _("Creator"),
|
||||
"modified": _("Modified"),
|
||||
}
|
||||
|
||||
|
||||
appbuilder.add_view_no_menu(DashboardModelViewAsync)
|
||||
|
||||
|
||||
class DashboardAddView(DashboardModelView):
|
||||
route_base = "/dashboardaddview"
|
||||
list_columns = [
|
||||
"id",
|
||||
"dashboard_link",
|
||||
"creator",
|
||||
"modified",
|
||||
"dashboard_title",
|
||||
"changed_on",
|
||||
"url",
|
||||
"changed_by_name",
|
||||
]
|
||||
show_columns = list(set(DashboardModelView.edit_columns + list_columns))
|
||||
|
||||
|
||||
appbuilder.add_view_no_menu(DashboardAddView)
|
||||
|
||||
|
||||
@talisman(force_https=False)
|
||||
@app.route("/health")
|
||||
def health():
|
||||
|
|
|
|||
|
|
@ -1,42 +0,0 @@
|
|||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
from flask import g, redirect
|
||||
from flask_appbuilder import expose
|
||||
from flask_appbuilder.security.decorators import has_access
|
||||
|
||||
from superset import appbuilder, db
|
||||
from superset.models import core as models
|
||||
|
||||
from .base import BaseSupersetView
|
||||
|
||||
|
||||
class Dashboard(BaseSupersetView):
|
||||
"""The base views for Superset!"""
|
||||
|
||||
@has_access
|
||||
@expose("/new/")
|
||||
def new(self): # pylint: disable=no-self-use
|
||||
"""Creates a new, blank dashboard and redirects to it in edit mode"""
|
||||
new_dashboard = models.Dashboard(
|
||||
dashboard_title="[ untitled dashboard ]", owners=[g.user]
|
||||
)
|
||||
db.session.add(new_dashboard)
|
||||
db.session.commit()
|
||||
return redirect(f"/superset/dashboard/{new_dashboard.id}/?edit=true")
|
||||
|
||||
|
||||
appbuilder.add_view_no_menu(Dashboard)
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
|
@ -0,0 +1,301 @@
|
|||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
import json
|
||||
import re
|
||||
|
||||
from flask import current_app, g, request
|
||||
from flask_appbuilder import ModelRestApi
|
||||
from flask_appbuilder.api import expose, protect, safe
|
||||
from flask_appbuilder.models.sqla.interface import SQLAInterface
|
||||
from marshmallow import fields, post_load, pre_load, Schema, ValidationError
|
||||
from marshmallow.validate import Length
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
|
||||
import superset.models.core as models
|
||||
from superset import appbuilder
|
||||
from superset.exceptions import SupersetException
|
||||
from superset.utils import core as utils
|
||||
from superset.views.base import BaseSupersetSchema
|
||||
|
||||
from .mixin import DashboardMixin
|
||||
|
||||
|
||||
class DashboardJSONMetadataSchema(Schema):
|
||||
timed_refresh_immune_slices = fields.List(fields.Integer())
|
||||
filter_scopes = fields.Dict()
|
||||
expanded_slices = fields.Dict()
|
||||
refresh_frequency = fields.Integer()
|
||||
default_filters = fields.Str()
|
||||
filter_immune_slice_fields = fields.Dict()
|
||||
stagger_refresh = fields.Boolean()
|
||||
stagger_time = fields.Integer()
|
||||
|
||||
|
||||
def validate_json(value):
|
||||
try:
|
||||
utils.validate_json(value)
|
||||
except SupersetException:
|
||||
raise ValidationError("JSON not valid")
|
||||
|
||||
|
||||
def validate_json_metadata(value):
|
||||
if not value:
|
||||
return
|
||||
try:
|
||||
value_obj = json.loads(value)
|
||||
except json.decoder.JSONDecodeError:
|
||||
raise ValidationError("JSON not valid")
|
||||
errors = DashboardJSONMetadataSchema(strict=True).validate(value_obj, partial=False)
|
||||
if errors:
|
||||
raise ValidationError(errors)
|
||||
|
||||
|
||||
def validate_slug_uniqueness(value):
|
||||
# slug is not required but must be unique
|
||||
if value:
|
||||
item = (
|
||||
current_app.appbuilder.get_session.query(models.Dashboard.id)
|
||||
.filter_by(slug=value)
|
||||
.one_or_none()
|
||||
)
|
||||
if item:
|
||||
raise ValidationError("Must be unique")
|
||||
|
||||
|
||||
def validate_owners(value):
|
||||
owner = (
|
||||
current_app.appbuilder.get_session.query(
|
||||
current_app.appbuilder.sm.user_model.id
|
||||
)
|
||||
.filter_by(id=value)
|
||||
.one_or_none()
|
||||
)
|
||||
if not owner:
|
||||
raise ValidationError(f"User {value} does not exist")
|
||||
|
||||
|
||||
class BaseDashboardSchema(BaseSupersetSchema):
|
||||
@staticmethod
|
||||
def set_owners(instance, owners):
|
||||
owner_objs = list()
|
||||
if g.user.id not in owners:
|
||||
owners.append(g.user.id)
|
||||
for owner_id in owners:
|
||||
user = current_app.appbuilder.get_session.query(
|
||||
current_app.appbuilder.sm.user_model
|
||||
).get(owner_id)
|
||||
owner_objs.append(user)
|
||||
instance.owners = owner_objs
|
||||
|
||||
@pre_load
|
||||
def pre_load(self, data): # pylint: disable=no-self-use
|
||||
data["slug"] = data.get("slug")
|
||||
data["owners"] = data.get("owners", [])
|
||||
if data["slug"]:
|
||||
data["slug"] = data["slug"].strip()
|
||||
data["slug"] = data["slug"].replace(" ", "-")
|
||||
data["slug"] = re.sub(r"[^\w\-]+", "", data["slug"])
|
||||
|
||||
|
||||
class DashboardPostSchema(BaseDashboardSchema):
|
||||
dashboard_title = fields.String(allow_none=True, validate=Length(0, 500))
|
||||
slug = fields.String(
|
||||
allow_none=True, validate=[Length(1, 255), validate_slug_uniqueness]
|
||||
)
|
||||
owners = fields.List(fields.Integer(validate=validate_owners))
|
||||
position_json = fields.String(validate=validate_json)
|
||||
css = fields.String()
|
||||
json_metadata = fields.String(validate=validate_json_metadata)
|
||||
published = fields.Boolean()
|
||||
|
||||
@post_load
|
||||
def make_object(self, data): # pylint: disable=no-self-use
|
||||
instance = models.Dashboard()
|
||||
self.set_owners(instance, data["owners"])
|
||||
for field in data:
|
||||
if field == "owners":
|
||||
self.set_owners(instance, data["owners"])
|
||||
else:
|
||||
setattr(instance, field, data.get(field))
|
||||
return instance
|
||||
|
||||
|
||||
class DashboardPutSchema(BaseDashboardSchema):
|
||||
dashboard_title = fields.String(allow_none=True, validate=Length(0, 500))
|
||||
slug = fields.String(allow_none=True, validate=Length(0, 255))
|
||||
owners = fields.List(fields.Integer(validate=validate_owners))
|
||||
position_json = fields.String(validate=validate_json)
|
||||
css = fields.String()
|
||||
json_metadata = fields.String(validate=validate_json_metadata)
|
||||
published = fields.Boolean()
|
||||
|
||||
@post_load
|
||||
def make_object(self, data): # pylint: disable=no-self-use
|
||||
if "owners" not in data and g.user not in self.instance.owners:
|
||||
self.instance.owners.append(g.user)
|
||||
for field in data:
|
||||
if field == "owners":
|
||||
self.set_owners(self.instance, data["owners"])
|
||||
else:
|
||||
setattr(self.instance, field, data.get(field))
|
||||
for slc in self.instance.slices:
|
||||
slc.owners = list(set(self.instance.owners) | set(slc.owners))
|
||||
return self.instance
|
||||
|
||||
|
||||
class DashboardRestApi(DashboardMixin, ModelRestApi):
|
||||
datamodel = SQLAInterface(models.Dashboard)
|
||||
|
||||
resource_name = "dashboard"
|
||||
allow_browser_login = True
|
||||
|
||||
class_permission_name = "DashboardModelView"
|
||||
method_permission_name = {
|
||||
"get_list": "list",
|
||||
"get": "show",
|
||||
"post": "add",
|
||||
"put": "edit",
|
||||
"delete": "delete",
|
||||
"info": "list",
|
||||
}
|
||||
exclude_route_methods = ("info",)
|
||||
show_columns = [
|
||||
"dashboard_title",
|
||||
"slug",
|
||||
"owners.id",
|
||||
"owners.username",
|
||||
"position_json",
|
||||
"css",
|
||||
"json_metadata",
|
||||
"published",
|
||||
"table_names",
|
||||
"charts",
|
||||
]
|
||||
|
||||
add_model_schema = DashboardPostSchema()
|
||||
edit_model_schema = DashboardPutSchema()
|
||||
|
||||
@expose("/", methods=["POST"])
|
||||
@protect()
|
||||
@safe
|
||||
def post(self):
|
||||
"""Creates a new dashboard
|
||||
---
|
||||
post:
|
||||
requestBody:
|
||||
description: Model schema
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/{{self.__class__.__name__}}.post'
|
||||
responses:
|
||||
201:
|
||||
description: Dashboard added
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
result:
|
||||
$ref: '#/components/schemas/{{self.__class__.__name__}}.post'
|
||||
400:
|
||||
$ref: '#/components/responses/400'
|
||||
401:
|
||||
$ref: '#/components/responses/401'
|
||||
422:
|
||||
$ref: '#/components/responses/422'
|
||||
500:
|
||||
$ref: '#/components/responses/500'
|
||||
"""
|
||||
if not request.is_json:
|
||||
return self.response_400(message="Request is not JSON")
|
||||
item = self.add_model_schema.load(request.json)
|
||||
# This validates custom Schema with custom validations
|
||||
if item.errors:
|
||||
return self.response_422(message=item.errors)
|
||||
try:
|
||||
self.datamodel.add(item.data, raise_exception=True)
|
||||
return self.response(
|
||||
201,
|
||||
result=self.add_model_schema.dump(item.data, many=False).data,
|
||||
id=item.data.id,
|
||||
)
|
||||
except SQLAlchemyError as e:
|
||||
return self.response_422(message=str(e))
|
||||
|
||||
@expose("/<pk>", methods=["PUT"])
|
||||
@protect()
|
||||
@safe
|
||||
def put(self, pk):
|
||||
"""Changes a dashboard
|
||||
---
|
||||
put:
|
||||
parameters:
|
||||
- in: path
|
||||
schema:
|
||||
type: integer
|
||||
name: pk
|
||||
requestBody:
|
||||
description: Model schema
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/{{self.__class__.__name__}}.put'
|
||||
responses:
|
||||
200:
|
||||
description: Item changed
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
result:
|
||||
$ref: '#/components/schemas/{{self.__class__.__name__}}.put'
|
||||
400:
|
||||
$ref: '#/components/responses/400'
|
||||
401:
|
||||
$ref: '#/components/responses/401'
|
||||
404:
|
||||
$ref: '#/components/responses/404'
|
||||
422:
|
||||
$ref: '#/components/responses/422'
|
||||
500:
|
||||
$ref: '#/components/responses/500'
|
||||
"""
|
||||
if not request.is_json:
|
||||
self.response_400(message="Request is not JSON")
|
||||
item = self.datamodel.get(pk, self._base_filters)
|
||||
if not item:
|
||||
return self.response_404()
|
||||
|
||||
item = self.edit_model_schema.load(request.json, instance=item)
|
||||
if item.errors:
|
||||
return self.response_422(message=item.errors)
|
||||
try:
|
||||
self.datamodel.edit(item.data, raise_exception=True)
|
||||
return self.response(
|
||||
200, result=self.edit_model_schema.dump(item.data, many=False).data
|
||||
)
|
||||
except SQLAlchemyError as e:
|
||||
return self.response_422(message=str(e))
|
||||
|
||||
|
||||
appbuilder.add_api(DashboardRestApi)
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
from sqlalchemy import and_, or_
|
||||
|
||||
from superset import db, security_manager
|
||||
from superset.models.core import Dashboard, FavStar, Slice
|
||||
from superset.views.base import BaseFilter
|
||||
|
||||
from ..base import get_user_roles
|
||||
|
||||
|
||||
class DashboardFilter(BaseFilter): # pylint: disable=too-few-public-methods
|
||||
"""
|
||||
List dashboards with the following criteria:
|
||||
1. Those which the user owns
|
||||
2. Those which the user has favorited
|
||||
3. Those which have been published (if they have access to at least one slice)
|
||||
|
||||
If the user is an admin show them all dashboards.
|
||||
This means they do not get curation but can still sort by "published"
|
||||
if they wish to see those dashboards which are published first
|
||||
"""
|
||||
|
||||
def apply(self, query, value):
|
||||
user_roles = [role.name.lower() for role in list(get_user_roles())]
|
||||
if "admin" in user_roles:
|
||||
return query
|
||||
|
||||
datasource_perms = security_manager.user_view_menu_names("datasource_access")
|
||||
schema_perms = security_manager.user_view_menu_names("schema_access")
|
||||
all_datasource_access = security_manager.all_datasource_access()
|
||||
published_dash_query = (
|
||||
db.session.query(Dashboard.id)
|
||||
.join(Dashboard.slices)
|
||||
.filter(
|
||||
and_(
|
||||
Dashboard.published == True, # pylint: disable=singleton-comparison
|
||||
or_(
|
||||
Slice.perm.in_(datasource_perms),
|
||||
Slice.schema_perm.in_(schema_perms),
|
||||
all_datasource_access,
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
users_favorite_dash_query = db.session.query(FavStar.obj_id).filter(
|
||||
and_(
|
||||
FavStar.user_id == security_manager.user_model.get_user_id(),
|
||||
FavStar.class_name == "Dashboard",
|
||||
)
|
||||
)
|
||||
owner_ids_query = (
|
||||
db.session.query(Dashboard.id)
|
||||
.join(Dashboard.owners)
|
||||
.filter(
|
||||
security_manager.user_model.id
|
||||
== security_manager.user_model.get_user_id()
|
||||
)
|
||||
)
|
||||
|
||||
query = query.filter(
|
||||
or_(
|
||||
Dashboard.id.in_(owner_ids_query),
|
||||
Dashboard.id.in_(published_dash_query),
|
||||
Dashboard.id.in_(users_favorite_dash_query),
|
||||
)
|
||||
)
|
||||
|
||||
return query
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
from flask_babel import lazy_gettext as _
|
||||
|
||||
from .filters import DashboardFilter
|
||||
|
||||
|
||||
class DashboardMixin: # pylint: disable=too-few-public-methods
|
||||
|
||||
list_title = _("Dashboards")
|
||||
show_title = _("Show Dashboard")
|
||||
add_title = _("Add Dashboard")
|
||||
edit_title = _("Edit Dashboard")
|
||||
|
||||
list_columns = ["dashboard_link", "creator", "published", "modified"]
|
||||
order_columns = ["dashboard_link", "modified", "published"]
|
||||
edit_columns = [
|
||||
"dashboard_title",
|
||||
"slug",
|
||||
"owners",
|
||||
"position_json",
|
||||
"css",
|
||||
"json_metadata",
|
||||
"published",
|
||||
]
|
||||
show_columns = edit_columns + ["table_names", "charts"]
|
||||
search_columns = ("dashboard_title", "slug", "owners", "published")
|
||||
add_columns = edit_columns
|
||||
base_order = ("changed_on", "desc")
|
||||
description_columns = {
|
||||
"position_json": _(
|
||||
"This json object describes the positioning of the widgets in "
|
||||
"the dashboard. It is dynamically generated when adjusting "
|
||||
"the widgets size and positions by using drag & drop in "
|
||||
"the dashboard view"
|
||||
),
|
||||
"css": _(
|
||||
"The CSS for individual dashboards can be altered here, or "
|
||||
"in the dashboard view where changes are immediately "
|
||||
"visible"
|
||||
),
|
||||
"slug": _("To get a readable URL for your dashboard"),
|
||||
"json_metadata": _(
|
||||
"This JSON object is generated dynamically when clicking "
|
||||
"the save or overwrite button in the dashboard view. It "
|
||||
"is exposed here for reference and for power users who may "
|
||||
"want to alter specific parameters."
|
||||
),
|
||||
"owners": _("Owners is a list of users who can alter the dashboard."),
|
||||
"published": _(
|
||||
"Determines whether or not this dashboard is "
|
||||
"visible in the list of all dashboards"
|
||||
),
|
||||
}
|
||||
base_filters = [["slice", DashboardFilter, lambda: []]]
|
||||
label_columns = {
|
||||
"dashboard_link": _("Dashboard"),
|
||||
"dashboard_title": _("Title"),
|
||||
"slug": _("Slug"),
|
||||
"charts": _("Charts"),
|
||||
"owners": _("Owners"),
|
||||
"creator": _("Creator"),
|
||||
"modified": _("Modified"),
|
||||
"position_json": _("Position JSON"),
|
||||
"css": _("CSS"),
|
||||
"json_metadata": _("JSON Metadata"),
|
||||
"table_names": _("Underlying Tables"),
|
||||
}
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
import re
|
||||
|
||||
from flask import g, redirect, request, Response
|
||||
from flask_appbuilder import expose
|
||||
from flask_appbuilder.actions import action
|
||||
from flask_appbuilder.models.sqla.interface import SQLAInterface
|
||||
from flask_appbuilder.security.decorators import has_access
|
||||
from flask_babel import gettext as __, lazy_gettext as _
|
||||
|
||||
import superset.models.core as models
|
||||
from superset import appbuilder, db, event_logger
|
||||
from superset.utils import core as utils
|
||||
|
||||
from ..base import (
|
||||
BaseSupersetView,
|
||||
check_ownership,
|
||||
DeleteMixin,
|
||||
generate_download_headers,
|
||||
SupersetModelView,
|
||||
)
|
||||
from .mixin import DashboardMixin
|
||||
|
||||
|
||||
class DashboardModelView(
|
||||
DashboardMixin, SupersetModelView, DeleteMixin
|
||||
): # pylint: disable=too-many-ancestors
|
||||
route_base = "/dashboard"
|
||||
datamodel = SQLAInterface(models.Dashboard)
|
||||
|
||||
@action("mulexport", __("Export"), __("Export dashboards?"), "fa-database")
|
||||
@staticmethod
|
||||
def mulexport(items):
|
||||
if not isinstance(items, list):
|
||||
items = [items]
|
||||
ids = "".join("&id={}".format(d.id) for d in items)
|
||||
return redirect("/dashboard/export_dashboards_form?{}".format(ids[1:]))
|
||||
|
||||
@event_logger.log_this
|
||||
@has_access
|
||||
@expose("/export_dashboards_form")
|
||||
def download_dashboards(self):
|
||||
if request.args.get("action") == "go":
|
||||
ids = request.args.getlist("id")
|
||||
return Response(
|
||||
models.Dashboard.export_dashboards(ids),
|
||||
headers=generate_download_headers("json"),
|
||||
mimetype="application/text",
|
||||
)
|
||||
return self.render_template(
|
||||
"superset/export_dashboards.html", dashboards_url="/dashboard/list"
|
||||
)
|
||||
|
||||
def pre_add(self, item):
|
||||
item.slug = item.slug or None
|
||||
if item.slug:
|
||||
item.slug = item.slug.strip()
|
||||
item.slug = item.slug.replace(" ", "-")
|
||||
item.slug = re.sub(r"[^\w\-]+", "", item.slug)
|
||||
if g.user not in item.owners:
|
||||
item.owners.append(g.user)
|
||||
utils.validate_json(item.json_metadata)
|
||||
utils.validate_json(item.position_json)
|
||||
owners = [o for o in item.owners]
|
||||
for slc in item.slices:
|
||||
slc.owners = list(set(owners) | set(slc.owners))
|
||||
|
||||
def pre_update(self, item):
|
||||
check_ownership(item)
|
||||
self.pre_add(item)
|
||||
|
||||
def pre_delete(self, item): # pylint: disable=no-self-use
|
||||
check_ownership(item)
|
||||
|
||||
|
||||
class Dashboard(BaseSupersetView):
|
||||
"""The base views for Superset!"""
|
||||
|
||||
@has_access
|
||||
@expose("/new/")
|
||||
def new(self): # pylint: disable=no-self-use
|
||||
"""Creates a new, blank dashboard and redirects to it in edit mode"""
|
||||
new_dashboard = models.Dashboard(
|
||||
dashboard_title="[ untitled dashboard ]", owners=[g.user]
|
||||
)
|
||||
db.session.add(new_dashboard)
|
||||
db.session.commit()
|
||||
return redirect(f"/superset/dashboard/{new_dashboard.id}/?edit=true")
|
||||
|
||||
|
||||
appbuilder.add_view_no_menu(Dashboard)
|
||||
|
||||
|
||||
class DashboardModelViewAsync(DashboardModelView): # pylint: disable=too-many-ancestors
|
||||
route_base = "/dashboardasync"
|
||||
list_columns = [
|
||||
"id",
|
||||
"dashboard_link",
|
||||
"creator",
|
||||
"modified",
|
||||
"dashboard_title",
|
||||
"changed_on",
|
||||
"url",
|
||||
"changed_by_name",
|
||||
]
|
||||
label_columns = {
|
||||
"dashboard_link": _("Dashboard"),
|
||||
"dashboard_title": _("Title"),
|
||||
"creator": _("Creator"),
|
||||
"modified": _("Modified"),
|
||||
}
|
||||
|
||||
|
||||
appbuilder.add_view_no_menu(DashboardModelViewAsync)
|
||||
|
||||
|
||||
class DashboardAddView(DashboardModelView): # pylint: disable=too-many-ancestors
|
||||
route_base = "/dashboardaddview"
|
||||
list_columns = [
|
||||
"id",
|
||||
"dashboard_link",
|
||||
"creator",
|
||||
"modified",
|
||||
"dashboard_title",
|
||||
"changed_on",
|
||||
"url",
|
||||
"changed_by_name",
|
||||
]
|
||||
show_columns = list(set(DashboardModelView.edit_columns + list_columns))
|
||||
|
||||
|
||||
appbuilder.add_view_no_menu(DashboardAddView)
|
||||
|
|
@ -18,6 +18,7 @@
|
|||
"""Unit tests for Superset"""
|
||||
import imp
|
||||
import json
|
||||
from typing import Union
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pandas as pd
|
||||
|
|
@ -43,6 +44,20 @@ class SupersetTestCase(TestCase):
|
|||
def create_app(self):
|
||||
return app
|
||||
|
||||
@staticmethod
|
||||
def create_user(
|
||||
username: str,
|
||||
password: str,
|
||||
role_name: str,
|
||||
first_name: str = "admin",
|
||||
last_name: str = "user",
|
||||
email: str = "admin@fab.org",
|
||||
) -> Union[ab_models.User, bool]:
|
||||
role_admin = security_manager.find_role(role_name)
|
||||
return security_manager.add_user(
|
||||
username, first_name, last_name, email, role_admin, password
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def create_druid_test_objects(cls):
|
||||
# create druid cluster and druid datasources
|
||||
|
|
|
|||
|
|
@ -0,0 +1,355 @@
|
|||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
"""Unit tests for Superset"""
|
||||
import json
|
||||
from typing import List
|
||||
|
||||
from flask_appbuilder.security.sqla import models as ab_models
|
||||
|
||||
from superset import db, security_manager
|
||||
from superset.models import core as models
|
||||
|
||||
from .base_tests import SupersetTestCase
|
||||
|
||||
|
||||
class DashboardApiTests(SupersetTestCase):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(DashboardApiTests, self).__init__(*args, **kwargs)
|
||||
|
||||
def insert_dashboard(
|
||||
self,
|
||||
dashboard_title: str,
|
||||
slug: str,
|
||||
owners: List[int],
|
||||
position_json: str = "",
|
||||
css: str = "",
|
||||
json_metadata: str = "",
|
||||
published: bool = False,
|
||||
) -> models.Dashboard:
|
||||
obj_owners = list()
|
||||
for owner in owners:
|
||||
user = db.session.query(security_manager.user_model).get(owner)
|
||||
obj_owners.append(user)
|
||||
dashboard = models.Dashboard(
|
||||
dashboard_title=dashboard_title,
|
||||
slug=slug,
|
||||
owners=obj_owners,
|
||||
position_json=position_json,
|
||||
css=css,
|
||||
json_metadata=json_metadata,
|
||||
published=published,
|
||||
)
|
||||
db.session.add(dashboard)
|
||||
db.session.commit()
|
||||
return dashboard
|
||||
|
||||
def get_user(self, username: str) -> ab_models.User:
|
||||
user = (
|
||||
db.session.query(security_manager.user_model)
|
||||
.filter_by(username=username)
|
||||
.one_or_none()
|
||||
)
|
||||
return user
|
||||
|
||||
def test_delete_dashboard(self):
|
||||
"""
|
||||
Dashboard API: Test delete
|
||||
"""
|
||||
admin_id = self.get_user("admin").id
|
||||
dashboard_id = self.insert_dashboard("title", "slug1", [admin_id]).id
|
||||
self.login(username="admin")
|
||||
uri = f"api/v1/dashboard/{dashboard_id}"
|
||||
rv = self.client.delete(uri)
|
||||
self.assertEqual(rv.status_code, 200)
|
||||
model = db.session.query(models.Dashboard).get(dashboard_id)
|
||||
self.assertEqual(model, None)
|
||||
|
||||
def test_delete_not_found_dashboard(self):
|
||||
"""
|
||||
Dashboard API: Test not found delete
|
||||
"""
|
||||
self.login(username="admin")
|
||||
dashboard_id = 1000
|
||||
uri = f"api/v1/dashboard/{dashboard_id}"
|
||||
rv = self.client.delete(uri)
|
||||
self.assertEqual(rv.status_code, 404)
|
||||
|
||||
def test_delete_dashboard_admin_not_owned(self):
|
||||
"""
|
||||
Dashboard API: Test admin delete not owned
|
||||
"""
|
||||
gamma_id = self.get_user("gamma").id
|
||||
dashboard_id = self.insert_dashboard("title", "slug1", [gamma_id]).id
|
||||
|
||||
self.login(username="admin")
|
||||
uri = f"api/v1/dashboard/{dashboard_id}"
|
||||
rv = self.client.delete(uri)
|
||||
self.assertEqual(rv.status_code, 200)
|
||||
model = db.session.query(models.Dashboard).get(dashboard_id)
|
||||
self.assertEqual(model, None)
|
||||
|
||||
def test_delete_dashboard_not_owned(self):
|
||||
"""
|
||||
Dashboard API: Test delete try not owned
|
||||
"""
|
||||
user_alpha1 = self.create_user(
|
||||
"alpha1", "password", "Alpha", email="alpha1@superset.org"
|
||||
)
|
||||
user_alpha2 = self.create_user(
|
||||
"alpha2", "password", "Alpha", email="alpha2@superset.org"
|
||||
)
|
||||
dashboard = self.insert_dashboard("title", "slug1", [user_alpha1.id])
|
||||
self.login(username="alpha2", password="password")
|
||||
uri = f"api/v1/dashboard/{dashboard.id}"
|
||||
rv = self.client.delete(uri)
|
||||
self.assertEqual(rv.status_code, 404)
|
||||
db.session.delete(dashboard)
|
||||
db.session.delete(user_alpha1)
|
||||
db.session.delete(user_alpha2)
|
||||
db.session.commit()
|
||||
|
||||
def test_create_dashboard(self):
|
||||
"""
|
||||
Dashboard API: Test create dashboard
|
||||
"""
|
||||
admin_id = self.get_user("admin").id
|
||||
dashboard_data = {
|
||||
"dashboard_title": "title1",
|
||||
"slug": "slug1",
|
||||
"owners": [admin_id],
|
||||
"position_json": '{"a": "A"}',
|
||||
"css": "css",
|
||||
"json_metadata": '{"b": "B"}',
|
||||
"published": True,
|
||||
}
|
||||
self.login(username="admin")
|
||||
uri = f"api/v1/dashboard/"
|
||||
rv = self.client.post(uri, json=dashboard_data)
|
||||
self.assertEqual(rv.status_code, 201)
|
||||
data = json.loads(rv.data.decode("utf-8"))
|
||||
model = db.session.query(models.Dashboard).get(data.get("id"))
|
||||
db.session.delete(model)
|
||||
db.session.commit()
|
||||
|
||||
def test_create_simple_dashboard(self):
|
||||
"""
|
||||
Dashboard API: Test create simple dashboard
|
||||
"""
|
||||
dashboard_data = {"dashboard_title": "title1"}
|
||||
self.login(username="admin")
|
||||
uri = f"api/v1/dashboard/"
|
||||
rv = self.client.post(uri, json=dashboard_data)
|
||||
self.assertEqual(rv.status_code, 201)
|
||||
data = json.loads(rv.data.decode("utf-8"))
|
||||
model = db.session.query(models.Dashboard).get(data.get("id"))
|
||||
db.session.delete(model)
|
||||
db.session.commit()
|
||||
|
||||
def test_create_dashboard_empty(self):
|
||||
"""
|
||||
Dashboard API: Test create empty
|
||||
"""
|
||||
dashboard_data = {}
|
||||
self.login(username="admin")
|
||||
uri = f"api/v1/dashboard/"
|
||||
rv = self.client.post(uri, json=dashboard_data)
|
||||
self.assertEqual(rv.status_code, 201)
|
||||
data = json.loads(rv.data.decode("utf-8"))
|
||||
model = db.session.query(models.Dashboard).get(data.get("id"))
|
||||
db.session.delete(model)
|
||||
db.session.commit()
|
||||
|
||||
dashboard_data = {"dashboard_title": ""}
|
||||
self.login(username="admin")
|
||||
uri = f"api/v1/dashboard/"
|
||||
rv = self.client.post(uri, json=dashboard_data)
|
||||
self.assertEqual(rv.status_code, 201)
|
||||
data = json.loads(rv.data.decode("utf-8"))
|
||||
model = db.session.query(models.Dashboard).get(data.get("id"))
|
||||
db.session.delete(model)
|
||||
db.session.commit()
|
||||
|
||||
def test_create_dashboard_validate_title(self):
|
||||
"""
|
||||
Dashboard API: Test create dashboard validate title
|
||||
"""
|
||||
dashboard_data = {"dashboard_title": "a" * 600}
|
||||
self.login(username="admin")
|
||||
uri = f"api/v1/dashboard/"
|
||||
rv = self.client.post(uri, json=dashboard_data)
|
||||
self.assertEqual(rv.status_code, 422)
|
||||
response = json.loads(rv.data.decode("utf-8"))
|
||||
expected_response = {
|
||||
"message": {"dashboard_title": ["Length must be between 0 and 500."]}
|
||||
}
|
||||
self.assertEqual(response, expected_response)
|
||||
|
||||
def test_create_dashboard_validate_slug(self):
|
||||
"""
|
||||
Dashboard API: Test create validate slug
|
||||
"""
|
||||
admin_id = self.get_user("admin").id
|
||||
dashboard = self.insert_dashboard("title1", "slug1", [admin_id])
|
||||
self.login(username="admin")
|
||||
|
||||
# Check for slug uniqueness
|
||||
dashboard_data = {"dashboard_title": "title2", "slug": "slug1"}
|
||||
uri = f"api/v1/dashboard/"
|
||||
rv = self.client.post(uri, json=dashboard_data)
|
||||
self.assertEqual(rv.status_code, 422)
|
||||
response = json.loads(rv.data.decode("utf-8"))
|
||||
expected_response = {"message": {"slug": ["Must be unique"]}}
|
||||
self.assertEqual(response, expected_response)
|
||||
|
||||
# Check for slug max size
|
||||
dashboard_data = {"dashboard_title": "title2", "slug": "a" * 256}
|
||||
uri = f"api/v1/dashboard/"
|
||||
rv = self.client.post(uri, json=dashboard_data)
|
||||
self.assertEqual(rv.status_code, 422)
|
||||
response = json.loads(rv.data.decode("utf-8"))
|
||||
expected_response = {"message": {"slug": ["Length must be between 1 and 255."]}}
|
||||
self.assertEqual(response, expected_response)
|
||||
|
||||
db.session.delete(dashboard)
|
||||
db.session.commit()
|
||||
|
||||
def test_create_dashboard_validate_owners(self):
|
||||
"""
|
||||
Dashboard API: Test create validate owners
|
||||
"""
|
||||
dashboard_data = {"dashboard_title": "title1", "owners": [1000]}
|
||||
self.login(username="admin")
|
||||
uri = f"api/v1/dashboard/"
|
||||
rv = self.client.post(uri, json=dashboard_data)
|
||||
self.assertEqual(rv.status_code, 422)
|
||||
response = json.loads(rv.data.decode("utf-8"))
|
||||
expected_response = {"message": {"owners": {"0": ["User 1000 does not exist"]}}}
|
||||
self.assertEqual(response, expected_response)
|
||||
|
||||
def test_create_dashboard_validate_json(self):
|
||||
"""
|
||||
Dashboard API: Test create validate json
|
||||
"""
|
||||
dashboard_data = {"dashboard_title": "title1", "position_json": '{"A:"a"}'}
|
||||
self.login(username="admin")
|
||||
uri = f"api/v1/dashboard/"
|
||||
rv = self.client.post(uri, json=dashboard_data)
|
||||
self.assertEqual(rv.status_code, 422)
|
||||
|
||||
dashboard_data = {"dashboard_title": "title1", "json_metadata": '{"A:"a"}'}
|
||||
self.login(username="admin")
|
||||
uri = f"api/v1/dashboard/"
|
||||
rv = self.client.post(uri, json=dashboard_data)
|
||||
self.assertEqual(rv.status_code, 422)
|
||||
|
||||
dashboard_data = {
|
||||
"dashboard_title": "title1",
|
||||
"json_metadata": '{"refresh_frequency": "A"}',
|
||||
}
|
||||
self.login(username="admin")
|
||||
uri = f"api/v1/dashboard/"
|
||||
rv = self.client.post(uri, json=dashboard_data)
|
||||
self.assertEqual(rv.status_code, 422)
|
||||
|
||||
def test_update_dashboard(self):
|
||||
"""
|
||||
Dashboard API: Test update
|
||||
"""
|
||||
admin_id = self.get_user("admin").id
|
||||
dashboard_id = self.insert_dashboard("title1", "slug1", [admin_id]).id
|
||||
dashboard_data = {
|
||||
"dashboard_title": "title1_changed",
|
||||
"slug": "slug1_changed",
|
||||
"owners": [admin_id],
|
||||
"position_json": '{"b": "B"}',
|
||||
"css": "css_changed",
|
||||
"json_metadata": '{"a": "A"}',
|
||||
"published": False,
|
||||
}
|
||||
self.login(username="admin")
|
||||
uri = f"api/v1/dashboard/{dashboard_id}"
|
||||
rv = self.client.put(uri, json=dashboard_data)
|
||||
self.assertEqual(rv.status_code, 200)
|
||||
model = db.session.query(models.Dashboard).get(dashboard_id)
|
||||
self.assertEqual(model.dashboard_title, "title1_changed")
|
||||
self.assertEqual(model.slug, "slug1_changed")
|
||||
self.assertEqual(model.position_json, '{"b": "B"}')
|
||||
self.assertEqual(model.css, "css_changed")
|
||||
self.assertEqual(model.json_metadata, '{"a": "A"}')
|
||||
self.assertEqual(model.published, False)
|
||||
db.session.delete(model)
|
||||
db.session.commit()
|
||||
|
||||
def test_update_dashboard_new_owner(self):
|
||||
"""
|
||||
Dashboard API: Test update set new owner to current user
|
||||
"""
|
||||
gamma_id = self.get_user("gamma").id
|
||||
admin = self.get_user("admin")
|
||||
dashboard_id = self.insert_dashboard("title1", "slug1", [gamma_id]).id
|
||||
dashboard_data = {"dashboard_title": "title1_changed"}
|
||||
self.login(username="admin")
|
||||
uri = f"api/v1/dashboard/{dashboard_id}"
|
||||
rv = self.client.put(uri, json=dashboard_data)
|
||||
self.assertEqual(rv.status_code, 200)
|
||||
model = db.session.query(models.Dashboard).get(dashboard_id)
|
||||
self.assertIn(admin, model.owners)
|
||||
for slc in model.slices:
|
||||
self.assertIn(admin, slc.owners)
|
||||
db.session.delete(model)
|
||||
db.session.commit()
|
||||
|
||||
def test_update_dashboard_slug_formatting(self):
|
||||
"""
|
||||
Dashboard API: Test update slug formatting
|
||||
"""
|
||||
admin_id = self.get_user("admin").id
|
||||
dashboard_id = self.insert_dashboard("title1", "slug1", [admin_id]).id
|
||||
dashboard_data = {"dashboard_title": "title1_changed", "slug": "slug1 changed"}
|
||||
self.login(username="admin")
|
||||
uri = f"api/v1/dashboard/{dashboard_id}"
|
||||
rv = self.client.put(uri, json=dashboard_data)
|
||||
self.assertEqual(rv.status_code, 200)
|
||||
model = db.session.query(models.Dashboard).get(dashboard_id)
|
||||
self.assertEqual(model.dashboard_title, "title1_changed")
|
||||
self.assertEqual(model.slug, "slug1-changed")
|
||||
db.session.delete(model)
|
||||
db.session.commit()
|
||||
|
||||
def test_update_dashboard_not_owned(self):
|
||||
"""
|
||||
Dashboard API: Test update slug formatting
|
||||
"""
|
||||
"""
|
||||
Dashboard API: Test delete try not owned
|
||||
"""
|
||||
user_alpha1 = self.create_user(
|
||||
"alpha1", "password", "Alpha", email="alpha1@superset.org"
|
||||
)
|
||||
user_alpha2 = self.create_user(
|
||||
"alpha2", "password", "Alpha", email="alpha2@superset.org"
|
||||
)
|
||||
dashboard = self.insert_dashboard("title", "slug1", [user_alpha1.id])
|
||||
self.login(username="alpha2", password="password")
|
||||
dashboard_data = {"dashboard_title": "title1_changed", "slug": "slug1 changed"}
|
||||
uri = f"api/v1/dashboard/{dashboard.id}"
|
||||
rv = self.client.put(uri, json=dashboard_data)
|
||||
self.assertEqual(rv.status_code, 404)
|
||||
db.session.delete(dashboard)
|
||||
db.session.delete(user_alpha1)
|
||||
db.session.delete(user_alpha2)
|
||||
db.session.commit()
|
||||
Loading…
Reference in New Issue