Skip to content

Commit

Permalink
Add a single endpoint that visualizes new table. [Hivepad itegration] (
Browse files Browse the repository at this point in the history
…#6)

* Create additional endpoint that vizualized the new table

* Unit tests

* Use proper schema depending on the engine
  • Loading branch information
bkyryliuk authored and bogdan-dbx committed Jun 23, 2020
1 parent bddb9ce commit 8fd37eb
Show file tree
Hide file tree
Showing 8 changed files with 222 additions and 77 deletions.
2 changes: 1 addition & 1 deletion superset/connectors/druid/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,12 @@
BaseSupersetView,
DatasourceFilter,
DeleteMixin,
get_datasource_exist_error_msg,
ListWidgetWithCheckboxes,
SupersetModelView,
validate_json,
YamlExportMixin,
)
from superset.views.utils import get_datasource_exist_error_msg

from . import models

Expand Down
3 changes: 1 addition & 2 deletions superset/connectors/sqla/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,13 @@
from superset.typing import FlaskResponse
from superset.utils import core as utils
from superset.views.base import (
create_table_permissions,
DatasourceFilter,
DeleteMixin,
ListWidgetWithCheckboxes,
SupersetModelView,
validate_sqlatable,
YamlExportMixin,
)
from superset.views.utils import create_table_permissions, validate_sqlatable

from . import models

Expand Down
2 changes: 1 addition & 1 deletion superset/datasets/commands/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
ForbiddenError,
UpdateFailedError,
)
from superset.views.base import get_datasource_exist_error_msg
from superset.views.utils import get_datasource_exist_error_msg


class DatabaseNotFoundValidationError(ValidationError):
Expand Down
7 changes: 3 additions & 4 deletions superset/security/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,15 +277,15 @@ def can_access_datasource(self, datasource: "BaseDatasource") -> bool:
"datasource_access", datasource.perm or ""
)

def get_datasource_access_error_msg(self, datasource: "BaseDatasource") -> str:
def get_datasource_access_error_msg(self, datasource_name: Optional[str]) -> str:
"""
Return the error message for the denied Superset datasource.
:param datasource: The denied Superset datasource
:param datasource_name: The denied Superset datasource name
:returns: The error message
"""

return f"""This endpoint requires the datasource {datasource.name}, database or
return f"""This endpoint requires the datasource {datasource_name}, database or
`all_datasource_access` permission"""

def get_datasource_access_link(self, datasource: "BaseDatasource") -> Optional[str]:
Expand Down Expand Up @@ -392,7 +392,6 @@ def rejected_tables(
) -> Set["Table"]:
"""
Return the list of rejected SQL tables.
:param sql: The SQL statement
:param database: The SQL database
:param schema: The SQL database schema
Expand Down
37 changes: 0 additions & 37 deletions superset/views/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@
get_feature_flags,
security_manager,
)
from superset.connectors.sqla import models
from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
from superset.exceptions import SupersetException, SupersetSecurityException
from superset.models.helpers import ImportMixin
Expand Down Expand Up @@ -193,42 +192,6 @@ def wraps(self: "BaseSupersetView", *args: Any, **kwargs: Any) -> FlaskResponse:
return functools.update_wrapper(wraps, f)


def get_datasource_exist_error_msg(full_name: str) -> str:
return __("Datasource %(name)s already exists", name=full_name)


def validate_sqlatable(table: models.SqlaTable) -> None:
"""Checks the table existence in the database."""
with db.session.no_autoflush:
table_query = db.session.query(models.SqlaTable).filter(
models.SqlaTable.table_name == table.table_name,
models.SqlaTable.schema == table.schema,
models.SqlaTable.database_id == table.database.id,
)
if db.session.query(table_query.exists()).scalar():
raise Exception(get_datasource_exist_error_msg(table.full_name))

# Fail before adding if the table can't be found
try:
table.get_sqla_table_object()
except Exception as ex:
logger.exception("Got an error in pre_add for %s", table.name)
raise Exception(
_(
"Table [%{table}s] could not be found, "
"please double check your "
"database connection, schema, and "
"table name, error: {}"
).format(table.name, str(ex))
)


def create_table_permissions(table: models.SqlaTable) -> None:
security_manager.add_permission_view_menu("datasource_access", table.get_perm())
if table.schema:
security_manager.add_permission_view_menu("schema_access", table.schema_perm)


def get_user_roles() -> List[Role]:
if g.user.is_anonymous:
public_role = conf.get("AUTH_ROLE_PUBLIC")
Expand Down
97 changes: 65 additions & 32 deletions superset/views/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import re
from collections import defaultdict
from contextlib import closing
from datetime import datetime, timedelta
from datetime import datetime
from typing import Any, cast, Dict, List, Optional, Union
from urllib import parse

Expand Down Expand Up @@ -93,7 +93,6 @@
BaseSupersetView,
check_ownership,
common_bootstrap_payload,
create_table_permissions,
CsvResponse,
data_payload_response,
generate_download_headers,
Expand All @@ -103,7 +102,6 @@
json_error_response,
json_errors_response,
json_success,
validate_sqlatable,
)
from superset.views.database.filters import DatabaseFilter
from superset.views.utils import (
Expand All @@ -112,12 +110,14 @@
bootstrap_user_data,
check_datasource_perms,
check_slice_perms,
create_if_not_exists_table,
get_cta_schema_name,
get_dashboard_extra_filters,
get_datasource_info,
get_form_data,
get_viz,
is_owner,
parse_table_full_name,
)
from superset.viz import BaseViz

Expand Down Expand Up @@ -563,6 +563,44 @@ def import_dashboards(self) -> FlaskResponse:
return redirect("/dashboard/list/")
return self.render_template("superset/import_dashboards.html")

@event_logger.log_this
@has_access
@expose(
"/explore_new/<int:database_id>/<datasource_type>/<datasource_name>/",
methods=["GET", "POST"],
)
def explore_new(
self, database_id: int, datasource_type: str, datasource_name: str,
) -> FlaskResponse:
"""Integration endpoint. Allows to visualize tables that were not precreated in Superset.
:param database_id: database id
:param datasource_type: table or druid
:param datasource_name: full name of the datasource, should include schema name if applicable
:return: redirects to the exploration page
"""
database_id = int(database_id)
if datasource_type != "table":
flash(__("Only table datasource type is supported"), "danger")
return redirect("/")

schema_name, table_name = parse_table_full_name(datasource_name)
database_obj = db.session.query(models.Database).get(database_id)
table = Table(table_name, schema_name)

if not security_manager.can_access_table(database_obj, table):
flash(
__(security_manager.get_datasource_access_error_msg(table)), "danger",
)
return redirect("/")

# overloading is_sqllab_view to be able to hide the temporary tables from the table list.
is_sqllab_view = request.args.get("is_sqllab_view") == "true"
table_id = create_if_not_exists_table(
database_id, schema_name, table_name, is_sqllab_view=is_sqllab_view
)
return redirect(f"/superset/explore/{datasource_type}/{table_id}")

@event_logger.log_this
@has_access
@expose("/explore/<datasource_type>/<int:datasource_id>/", methods=["GET", "POST"])
Expand Down Expand Up @@ -623,7 +661,7 @@ def explore(
not security_manager.can_access_datasource(datasource)
):
flash(
__(security_manager.get_datasource_access_error_msg(datasource)),
__(security_manager.get_datasource_access_error_msg(datasource.name)),
"danger",
)
return redirect(
Expand Down Expand Up @@ -1631,7 +1669,9 @@ def dashboard(self, dashboard_id_or_slug: str) -> FlaskResponse:
):
flash(
__(
security_manager.get_datasource_access_error_msg(datasource)
security_manager.get_datasource_access_error_msg(
datasource.name
)
),
"danger",
)
Expand All @@ -1652,6 +1692,9 @@ def dashboard(self, dashboard_id_or_slug: str) -> FlaskResponse:
) and security_manager.can_access("can_save_dash", "Superset")
dash_save_perm = security_manager.can_access("can_save_dash", "Superset")
superset_can_explore = security_manager.can_access("can_explore", "Superset")
superset_can_explore_new = security_manager.can_access(
"can_explore_new", "Superset"
)
superset_can_csv = security_manager.can_access("can_csv", "Superset")
slice_can_edit = security_manager.can_access("can_edit", "SliceModelView")

Expand Down Expand Up @@ -1681,6 +1724,7 @@ def dashboard(**kwargs: Any) -> None:
"dash_save_perm": dash_save_perm,
"dash_edit_perm": dash_edit_perm,
"superset_can_explore": superset_can_explore,
"superset_can_explore_new": superset_can_explore_new,
"superset_can_csv": superset_can_csv,
"slice_can_edit": slice_can_edit,
}
Expand Down Expand Up @@ -1793,33 +1837,22 @@ def sqllab_table_viz(self) -> FlaskResponse:
* templateParams - params for the Jinja templating syntax, optional
:return: Response
"""
data = json.loads(request.form["data"])
table_name = data["datasourceName"]
database_id = data["dbId"]
table = (
db.session.query(SqlaTable)
.filter_by(database_id=database_id, table_name=table_name)
.one_or_none()
)
if not table:
# Create table if doesn't exist.
with db.session.no_autoflush:
table = SqlaTable(table_name=table_name, owners=[g.user])
table.database_id = database_id
table.database = (
db.session.query(models.Database).filter_by(id=database_id).one()
)
table.schema = data.get("schema")
table.template_params = data.get("templateParams")
# needed for the table validation.
validate_sqlatable(table)

db.session.add(table)
table.fetch_metadata()
create_table_permissions(table)
db.session.commit()

return json_success(json.dumps({"table_id": table.id}))
data = json.loads(request.form.get("data", ""))
database_id = data.get("dbId")
table_name = data.get("datasourceName")
schema_name = data.get("schema")
# overloading is_sqllab_view to be able to hide the temporary tables from the table list.
is_sqllab_view = request.args.get("is_sqllab_view") == "true"
template_params = data.get("templateParams")

table_id = create_if_not_exists_table(
database_id,
schema_name,
table_name,
template_params=template_params,
is_sqllab_view=is_sqllab_view,
)
return json_success(json.dumps({"table_id": table_id}))

@has_access
@expose("/sqllab_viz/", methods=["POST"])
Expand Down
84 changes: 84 additions & 0 deletions superset/views/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from flask import g, request
from flask_appbuilder.security.sqla import models as ab_models
from flask_appbuilder.security.sqla.models import User
from flask_babel import gettext as __, lazy_gettext as _

import superset.models.core as models
from superset import (
Expand All @@ -37,6 +38,7 @@
security_manager,
)
from superset.connectors.connector_registry import ConnectorRegistry
from superset.connectors.sqla.models import SqlaTable
from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
from superset.exceptions import SupersetException, SupersetSecurityException
from superset.legacy import update_time_range
Expand Down Expand Up @@ -496,3 +498,85 @@ def get_cta_schema_name(
if not func:
return None
return func(database, user, schema, sql)


def parse_table_full_name(full_table_name: str) -> Tuple[Optional[str], str]:
"""Parses full table name into components like table name, schema name.
Note the table name conforms to the [[cluster.]schema.]table construct.
"""
table_name_pieces = full_table_name.split(".")
if len(table_name_pieces) == 3:
return table_name_pieces[1], table_name_pieces[1]
if len(table_name_pieces) == 2:
return table_name_pieces[0], table_name_pieces[1]
return None, table_name_pieces[0]


def create_table_permissions(table: SqlaTable) -> None:
security_manager.add_permission_view_menu("datasource_access", table.get_perm())
if table.schema:
security_manager.add_permission_view_menu("schema_access", table.schema_perm)


def get_datasource_exist_error_msg(full_name: str) -> str:
return __("Datasource %(name)s already exists", name=full_name)


def validate_sqlatable(table: SqlaTable) -> None:
"""Checks the table existence in the database."""
with db.session.no_autoflush:
table_query = db.session.query(SqlaTable).filter(
SqlaTable.table_name == table.table_name,
SqlaTable.schema == table.schema,
SqlaTable.database_id == table.database.id,
)
if db.session.query(table_query.exists()).scalar():
raise Exception(get_datasource_exist_error_msg(table.full_name))

# Fail before adding if the table can't be found
try:
table.get_sqla_table_object()
except Exception as ex:
logger.exception("Got an error in pre_add for %s", table.name)
raise Exception(
_(
"Table [%{table}s] could not be found, "
"please double check your "
"database connection, schema, and "
"table name, error: {}"
).format(table.name, str(ex))
)


def create_if_not_exists_table(
database_id: int,
schema_name: Optional[str],
table_name: str,
template_params: Optional[str] = None,
is_sqllab_view: bool = False,
) -> int:
table = (
db.session.query(SqlaTable)
.filter_by(database_id=database_id, schema=schema_name, table_name=table_name)
.one_or_none()
)
if not table:
# Create table if doesn't exist.
with db.session.no_autoflush:
table = SqlaTable(table_name=table_name, owners=[g.user])
table.database_id = database_id
table.database = (
db.session.query(models.Database).filter_by(id=database_id).one()
)
table.schema = schema_name
table.template_params = template_params
table.is_sqllab_view = is_sqllab_view
# needed for the table validation.
validate_sqlatable(table)

db.session.add(table)
table.fetch_metadata()
create_table_permissions(table)
db.session.commit()
return table.id
Loading

0 comments on commit 8fd37eb

Please sign in to comment.