mirror of
https://github.com/apache/superset.git
synced 2026-06-01 05:39:17 +00:00
* Chinese page * Using react-intl-universal to improve multi language in react page * Using react-intl-universal to improve multi language in react page * react_intl_universal * change * change * change * change * change * change * change * merge * multiple page in js * merge * merge * merge * merge * Js Translations * JS Translation * JS Translations * Js translation * JS translations * JS translations * Js translaion * JS en Translation * JS Translation * upgrade document Fixing the damn build (#3179) * Fixing the build * Going deeper [bugfix] only filterable columns should show up in FilterBox list (#3105) * [bugfix] only filterable columns should show up in FilterBox list * Touchups Datasource cannot be empty (#3035) add title description to model view (#3045) * add title description to model view * add missing import Add 'show/hide totals' option to pivot table vis (#3101) [bugfix] numeric value for date fields in table viz (#3036) Bug was present only when using the NOT GROUPED BY option fixes https://github.com/ApacheInfra/superset/issues/3027 fix hive.fetch_logs (#2968) add Zalando to the list of organizations (#3171) docs: fixup installation examples code indentation (#3169) [bugfix] fix bar order (#3180) [bugfix] visualize flow error: 'Metric x is not valid' (#3181) The metric name in the frontend doesn't match the one generated on the backend. It turns out the explore view will default to the first metric so specifying one isn't needed. Fix the segment interval for pulling metadata (#3174) The end of the interval would be on the truncated today date, which means that you will exclude today. If your realtime ingestion job runs shorter than a day, the metadata cannot be pulled from the druid cluster. Bump cryptography to 1.9 (#3065) As 1.7.2 doesn't compile here with openssl 1.1.0f Escaping the user's SQL in the explore view (#3186) * Escaping the user's SQL in the explore view When executing SQL from SQL Lab, we use a lower level API to the database which doesn't require escaping the SQL. When going through the explore view, the stack chain leading to the same method may need escaping depending on how the DBAPI driver is written, and that is the case for Presto (and perhaps other drivers). * Using regex to avoid doubling doubles [sqllab] improve Hive support (#3187) * [sqllab] improve Hive support * Fix "Transport not open" bug * Getting progress bar to show * Bump pyhive to 0.4.0 * Getting [Track Job] button to show * Fix testzz Add BigQuery engine specifications (#3193) As contributed by @mxmzdlv on issue #945 [bugfix] fix merge conflict that broke Hive support (#3196) Adding 'apache' to docs (#3194) [druid] Allow custom druid postaggregators (#3146) * [druid] Allow custom druid postaggregators Also, fix the postaggregation for approxHistogram quantiles so it adds the dependent field and that can show up in the graphs/tables. In general, postAggregators add significant power, we should probably support including custom postAggregators. Plywood has standard postAggregators here, and a customAggregator escape hatch that allows you to define custom postAggregators. This commit adds a similar capability for Superset and a additional field/fields/fieldName breakdown of the typical naming for dependent aggregations, which should make it significantly easier to develop approxHistogram and custom postAggregation-required dashboards. * [druid] Minor style cleanup in tests file. * [druid] Apply code review suggestions * break out CustomPostAggregator into separate class. This just cleans up the creation of the postaggregator a little bit. * minor style issues. * move the function around so the git diff is more readable add combine config for metrics in pivot table (#3086) * add combine config for metrics in pivot table * change method to stack/unstack * update backendSync Autofocus search input in VizTypeControl modal onEnter (#2929) Speed up JS build time (#3203) Also bumping a few related libs JS Translation JS translations js translation fix issue 3204 (#3205) [bugfix] capture Hive job_id pre-url transformation (#3213) js translation fix issue 3204 (#3205) [bugfix] capture Hive job_id pre-url transformation (#3213) [docs] update url in CONTRIBUTING.md (#3212) [sqllab/cosmetics] add margin-top for labels in query history (#3222) [explore] nvd3 sort values in rich tooltip (#3197) [sqllab] fix UI shows 'The query returned no results' momentarily (#3214) this is visible when running async queries between the fetching and success state as the rows are getting cached in the component [explore] DatasourceControl to pick datasource in modal (#3210) * [explore] DatasourceControl to pick datasource in modal Makes it easier to change datasource, also makes it such that the list of all datasources doesn't need to be loaded upfront. * Adding more metadata * Js translation * js tran * js trans * js trans * js tran * js trans * js trans * js tran * js translation * js trans * js translation * try load language pack async * Backend translations things * create language pack inside common data * performance improvement for js i18n. - js bundle should not contain localized content - we populate translation content from server-side, in boostrap.common.language_pack - in client-side, use promise to wrap around translation content. text will be translated after translation content arrived/parsed. - fix linting * fix Timer unit test * 1. add global hook for all tests, to make translation pack avaialble before each test starts. 2. fix unit test for Timer component 3. remove noused method get_locale, and modules 4. fix page reload after user change page language * parse and build i18n dictionary as a module * fix sync-backend task, which should run without DOM
351 lines
12 KiB
Python
351 lines
12 KiB
Python
import functools
|
|
import json
|
|
import logging
|
|
import traceback
|
|
|
|
from flask import g, redirect, Response, flash, abort, get_flashed_messages
|
|
from flask_babel import gettext as __
|
|
from flask_babel import lazy_gettext as _
|
|
from flask_babel import get_locale
|
|
|
|
from flask_appbuilder import BaseView
|
|
from flask_appbuilder import ModelView
|
|
from flask_appbuilder.widgets import ListWidget
|
|
from flask_appbuilder.actions import action
|
|
from flask_appbuilder.models.sqla.filters import BaseFilter
|
|
|
|
from superset import appbuilder, conf, db, utils, sm, sql_parse
|
|
from superset.connectors.connector_registry import ConnectorRegistry
|
|
from superset.connectors.sqla.models import SqlaTable
|
|
from superset.translations.utils import get_language_pack
|
|
|
|
FRONTEND_CONF_KEYS = ('SUPERSET_WEBSERVER_TIMEOUT',)
|
|
|
|
|
|
def get_error_msg():
|
|
if conf.get("SHOW_STACKTRACE"):
|
|
error_msg = traceback.format_exc()
|
|
else:
|
|
error_msg = "FATAL ERROR \n"
|
|
error_msg += (
|
|
"Stacktrace is hidden. Change the SHOW_STACKTRACE "
|
|
"configuration setting to enable it")
|
|
return error_msg
|
|
|
|
|
|
def json_error_response(msg=None, status=500, stacktrace=None, payload=None):
|
|
if not payload:
|
|
payload = {'error': str(msg)}
|
|
if stacktrace:
|
|
payload['stacktrace'] = stacktrace
|
|
return Response(
|
|
json.dumps(payload, default=utils.json_iso_dttm_ser),
|
|
status=status, mimetype="application/json")
|
|
|
|
|
|
def api(f):
|
|
"""
|
|
A decorator to label an endpoint as an API. Catches uncaught exceptions and
|
|
return the response in the JSON format
|
|
"""
|
|
def wraps(self, *args, **kwargs):
|
|
try:
|
|
return f(self, *args, **kwargs)
|
|
except Exception as e:
|
|
logging.exception(e)
|
|
return json_error_response(get_error_msg())
|
|
|
|
return functools.update_wrapper(wraps, f)
|
|
|
|
|
|
def get_datasource_exist_error_mgs(full_name):
|
|
return __("Datasource %(name)s already exists", name=full_name)
|
|
|
|
|
|
def get_user_roles():
|
|
if g.user.is_anonymous():
|
|
public_role = conf.get('AUTH_ROLE_PUBLIC')
|
|
return [appbuilder.sm.find_role(public_role)] if public_role else []
|
|
return g.user.roles
|
|
|
|
|
|
class BaseSupersetView(BaseView):
|
|
def can_access(self, permission_name, view_name, user=None):
|
|
if not user:
|
|
user = g.user
|
|
return utils.can_access(
|
|
appbuilder.sm, permission_name, view_name, user)
|
|
|
|
def all_datasource_access(self, user=None):
|
|
return self.can_access(
|
|
"all_datasource_access", "all_datasource_access", user=user)
|
|
|
|
def database_access(self, database, user=None):
|
|
return (
|
|
self.can_access(
|
|
"all_database_access", "all_database_access", user=user) or
|
|
self.can_access("database_access", database.perm, user=user)
|
|
)
|
|
|
|
def schema_access(self, datasource, user=None):
|
|
return (
|
|
self.database_access(datasource.database, user=user) or
|
|
self.all_datasource_access(user=user) or
|
|
self.can_access("schema_access", datasource.schema_perm, user=user)
|
|
)
|
|
|
|
def datasource_access(self, datasource, user=None):
|
|
return (
|
|
self.schema_access(datasource, user=user) or
|
|
self.can_access("datasource_access", datasource.perm, user=user)
|
|
)
|
|
|
|
def datasource_access_by_name(
|
|
self, database, datasource_name, schema=None):
|
|
if self.database_access(database) or self.all_datasource_access():
|
|
return True
|
|
|
|
schema_perm = utils.get_schema_perm(database, schema)
|
|
if schema and self.can_access('schema_access', schema_perm):
|
|
return True
|
|
|
|
datasources = ConnectorRegistry.query_datasources_by_name(
|
|
db.session, database, datasource_name, schema=schema)
|
|
for datasource in datasources:
|
|
if self.can_access("datasource_access", datasource.perm):
|
|
return True
|
|
return False
|
|
|
|
def datasource_access_by_fullname(
|
|
self, database, full_table_name, schema):
|
|
table_name_pieces = full_table_name.split(".")
|
|
if len(table_name_pieces) == 2:
|
|
table_schema = table_name_pieces[0]
|
|
table_name = table_name_pieces[1]
|
|
else:
|
|
table_schema = schema
|
|
table_name = table_name_pieces[0]
|
|
return self.datasource_access_by_name(
|
|
database, table_name, schema=table_schema)
|
|
|
|
def rejected_datasources(self, sql, database, schema):
|
|
superset_query = sql_parse.SupersetQuery(sql)
|
|
return [
|
|
t for t in superset_query.tables if not
|
|
self.datasource_access_by_fullname(database, t, schema)]
|
|
|
|
def user_datasource_perms(self):
|
|
datasource_perms = set()
|
|
for r in g.user.roles:
|
|
for perm in r.permissions:
|
|
if (
|
|
perm.permission and
|
|
'datasource_access' == perm.permission.name):
|
|
datasource_perms.add(perm.view_menu.name)
|
|
return datasource_perms
|
|
|
|
def schemas_accessible_by_user(self, database, schemas):
|
|
if self.database_access(database) or self.all_datasource_access():
|
|
return schemas
|
|
|
|
subset = set()
|
|
for schema in schemas:
|
|
schema_perm = utils.get_schema_perm(database, schema)
|
|
if self.can_access('schema_access', schema_perm):
|
|
subset.add(schema)
|
|
|
|
perms = self.user_datasource_perms()
|
|
if perms:
|
|
tables = (
|
|
db.session.query(SqlaTable)
|
|
.filter(
|
|
SqlaTable.perm.in_(perms),
|
|
SqlaTable.database_id == database.id,
|
|
)
|
|
.all()
|
|
)
|
|
for t in tables:
|
|
if t.schema:
|
|
subset.add(t.schema)
|
|
return sorted(list(subset))
|
|
|
|
def accessible_by_user(self, database, datasource_names, schema=None):
|
|
if self.database_access(database) or self.all_datasource_access():
|
|
return datasource_names
|
|
|
|
if schema:
|
|
schema_perm = utils.get_schema_perm(database, schema)
|
|
if self.can_access('schema_access', schema_perm):
|
|
return datasource_names
|
|
|
|
user_perms = self.user_datasource_perms()
|
|
user_datasources = ConnectorRegistry.query_datasources_by_permissions(
|
|
db.session, database, user_perms)
|
|
if schema:
|
|
names = {
|
|
d.table_name
|
|
for d in user_datasources if d.schema == schema}
|
|
return [d for d in datasource_names if d in names]
|
|
else:
|
|
full_names = {d.full_name for d in user_datasources}
|
|
return [d for d in datasource_names if d in full_names]
|
|
|
|
def common_bootsrap_payload(self):
|
|
"""Common data always sent to the client"""
|
|
messages = get_flashed_messages(with_categories=True)
|
|
locale = str(get_locale())
|
|
return {
|
|
'flash_messages': messages,
|
|
'conf': {k: conf.get(k) for k in FRONTEND_CONF_KEYS},
|
|
'locale': locale,
|
|
'language_pack': get_language_pack(locale),
|
|
}
|
|
|
|
|
|
class SupersetModelView(ModelView):
|
|
page_size = 100
|
|
|
|
|
|
class ListWidgetWithCheckboxes(ListWidget):
|
|
"""An alternative to list view that renders Boolean fields as checkboxes
|
|
|
|
Works in conjunction with the `checkbox` view."""
|
|
template = 'superset/fab_overrides/list_with_checkboxes.html'
|
|
|
|
|
|
def validate_json(form, field): # noqa
|
|
try:
|
|
json.loads(field.data)
|
|
except Exception as e:
|
|
logging.exception(e)
|
|
raise Exception(_("json isn't valid"))
|
|
|
|
|
|
class DeleteMixin(object):
|
|
def _delete(self, pk):
|
|
"""
|
|
Delete function logic, override to implement diferent logic
|
|
deletes the record with primary_key = pk
|
|
|
|
:param pk:
|
|
record primary key to delete
|
|
"""
|
|
item = self.datamodel.get(pk, self._base_filters)
|
|
if not item:
|
|
abort(404)
|
|
try:
|
|
self.pre_delete(item)
|
|
except Exception as e:
|
|
flash(str(e), "danger")
|
|
else:
|
|
view_menu = sm.find_view_menu(item.get_perm())
|
|
pvs = sm.get_session.query(sm.permissionview_model).filter_by(
|
|
view_menu=view_menu).all()
|
|
|
|
schema_view_menu = None
|
|
if hasattr(item, 'schema_perm'):
|
|
schema_view_menu = sm.find_view_menu(item.schema_perm)
|
|
|
|
pvs.extend(sm.get_session.query(
|
|
sm.permissionview_model).filter_by(
|
|
view_menu=schema_view_menu).all())
|
|
|
|
if self.datamodel.delete(item):
|
|
self.post_delete(item)
|
|
|
|
for pv in pvs:
|
|
sm.get_session.delete(pv)
|
|
|
|
if view_menu:
|
|
sm.get_session.delete(view_menu)
|
|
|
|
if schema_view_menu:
|
|
sm.get_session.delete(schema_view_menu)
|
|
|
|
sm.get_session.commit()
|
|
|
|
flash(*self.datamodel.message)
|
|
self.update_redirect()
|
|
|
|
@action(
|
|
"muldelete",
|
|
__("Delete"),
|
|
__("Delete all Really?"),
|
|
"fa-trash",
|
|
single=False
|
|
)
|
|
def muldelete(self, items):
|
|
if not items:
|
|
abort(404)
|
|
for item in items:
|
|
try:
|
|
self.pre_delete(item)
|
|
except Exception as e:
|
|
flash(str(e), "danger")
|
|
else:
|
|
self._delete(item.id)
|
|
self.update_redirect()
|
|
return redirect(self.get_redirect())
|
|
|
|
|
|
class SupersetFilter(BaseFilter):
|
|
|
|
"""Add utility function to make BaseFilter easy and fast
|
|
|
|
These utility function exist in the SecurityManager, but would do
|
|
a database round trip at every check. Here we cache the role objects
|
|
to be able to make multiple checks but query the db only once
|
|
"""
|
|
|
|
def get_user_roles(self):
|
|
return get_user_roles()
|
|
|
|
def get_all_permissions(self):
|
|
"""Returns a set of tuples with the perm name and view menu name"""
|
|
perms = set()
|
|
for role in self.get_user_roles():
|
|
for perm_view in role.permissions:
|
|
t = (perm_view.permission.name, perm_view.view_menu.name)
|
|
perms.add(t)
|
|
return perms
|
|
|
|
def has_role(self, role_name_or_list):
|
|
"""Whether the user has this role name"""
|
|
if not isinstance(role_name_or_list, list):
|
|
role_name_or_list = [role_name_or_list]
|
|
return any(
|
|
[r.name in role_name_or_list for r in self.get_user_roles()])
|
|
|
|
def has_perm(self, permission_name, view_menu_name):
|
|
"""Whether the user has this perm"""
|
|
return (permission_name, view_menu_name) in self.get_all_permissions()
|
|
|
|
def get_view_menus(self, permission_name):
|
|
"""Returns the details of view_menus for a perm name"""
|
|
vm = set()
|
|
for perm_name, vm_name in self.get_all_permissions():
|
|
if perm_name == permission_name:
|
|
vm.add(vm_name)
|
|
return vm
|
|
|
|
def has_all_datasource_access(self):
|
|
return (
|
|
self.has_role(['Admin', 'Alpha']) or
|
|
self.has_perm('all_datasource_access', 'all_datasource_access'))
|
|
|
|
|
|
class DatasourceFilter(SupersetFilter):
|
|
def apply(self, query, func): # noqa
|
|
if self.has_all_datasource_access():
|
|
return query
|
|
perms = self.get_view_menus('datasource_access')
|
|
# TODO(bogdan): add `schema_access` support here
|
|
return query.filter(self.model.perm.in_(perms))
|
|
|
|
|
|
class CsvResponse(Response):
|
|
"""
|
|
Override Response to take into account csv encoding from config.py
|
|
"""
|
|
charset = conf.get('CSV_EXPORT').get('encoding', 'utf-8')
|