This is an automated email from the ASF dual-hosted git repository.

johnbodley pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-superset.git


The following commit(s) were added to refs/heads/master by this push:
     new cf87173  [wtforms] Strip leading/trailing whitespace (#7084)
cf87173 is described below

commit cf87173c211eeb80b0099c5d6e4b8af910fd4b82
Author: John Bodley <[email protected]>
AuthorDate: Tue Apr 23 12:04:50 2019 -0700

    [wtforms] Strip leading/trailing whitespace (#7084)
---
 .../migrations/versions/d94d33dbe938_form_strip.py | 193 +++++++++++++++++++++
 superset/views/base.py                             |  27 +++
 tox.ini                                            |   2 +
 3 files changed, 222 insertions(+)

diff --git a/superset/migrations/versions/d94d33dbe938_form_strip.py 
b/superset/migrations/versions/d94d33dbe938_form_strip.py
new file mode 100644
index 0000000..b34bbaa
--- /dev/null
+++ b/superset/migrations/versions/d94d33dbe938_form_strip.py
@@ -0,0 +1,193 @@
+# 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.
+"""form strip
+
+Revision ID: d94d33dbe938
+Revises: 80aa3f04bc82
+Create Date: 2019-03-21 10:22:01.610217
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = 'd94d33dbe938'
+down_revision = '80aa3f04bc82'
+
+from alembic import op
+from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy import Column, Integer, String, Text
+
+from superset import db
+from superset.utils.core import MediumText
+
+Base = declarative_base()
+
+
+class BaseColumnMixin(object):
+    id = Column(Integer, primary_key=True)
+    column_name = Column(String(255))
+    description = Column(Text)
+    type = Column(String(32))
+    verbose_name = Column(String(1024))
+
+
+class BaseDatasourceMixin(object):
+    id = Column(Integer, primary_key=True)
+    description = Column(Text)
+
+
+class BaseMetricMixin(object):
+    id = Column(Integer, primary_key=True)
+    d3format = Column(String(128))
+    description = Column(Text)
+    metric_name = Column(String(512))
+    metric_type = Column(String(32))
+    verbose_name = Column(String(1024))
+    warning_text = Column(Text)
+
+
+class Annotation(Base):
+    __tablename__ = 'annotation'
+
+    id = Column(Integer, primary_key=True)
+    long_descr = Column(Text)
+    json_metadata = Column(Text)
+    short_descr = Column(String(500))
+
+
+class Dashboard(Base):
+    __tablename__ = 'dashboards'
+
+    id = Column(Integer, primary_key=True)
+    css = Column(Text)
+    dashboard_title = Column(String(500))
+    description = Column(Text)
+    json_metadata = Column(Text)
+    position_json = Column(MediumText())
+    slug = Column(String(255))
+
+
+class Database(Base):
+    __tablename__ = 'dbs'
+
+    id = Column(Integer, primary_key=True)
+    database_name = Column(String(250))
+    extra = Column(Text)
+    force_ctas_schema = Column(String(250))
+    sqlalchemy_uri = Column(String(1024))
+    verbose_name = Column(String(250))
+
+
+class DruidCluster(Base):
+    __tablename__ = 'clusters'
+
+    id = Column(Integer, primary_key=True)
+    broker_host = Column(String(255))
+    broker_endpoint = Column(String(255))
+    cluster_name = Column(String(250))
+    verbose_name = Column(String(250))
+
+
+class DruidColumn(BaseColumnMixin, Base):
+    __tablename__ = 'columns'
+
+    dimension_spec_json = Column(Text)
+
+
+class DruidDatasource(BaseDatasourceMixin, Base):
+    __tablename__ = 'datasources'
+
+    datasource_name = Column(String(255))
+    default_endpoint = Column(Text)
+    fetch_values_from = Column(String(100))
+
+
+class DruidMetric(BaseMetricMixin, Base):
+    __tablename__ = 'metrics'
+
+    json = Column(Text)
+
+
+class Slice(Base):
+    __tablename__ = 'slices'
+
+    id = Column(Integer, primary_key=True)
+    description = Column(Text)
+    params = Column(Text)
+    slice_name = Column(String(250))
+    viz_type = Column(String(250))
+
+
+class SqlaTable(BaseDatasourceMixin, Base):
+    __tablename__ = 'tables'
+
+    default_endpoint = Column(MediumText())
+    fetch_values_predicate = Column(String(1000))
+    main_dttm_col = Column(String(250))
+    schema = Column(String(255))
+    sql = Column(Text)
+    table_name = Column(String(250))
+    template_params = Column(Text)
+
+
+class SqlMetric(BaseMetricMixin, Base):
+    __tablename__ = 'sql_metrics'
+
+    expression = Column(Text)
+
+
+class TableColumn(BaseColumnMixin, Base):
+    __tablename__ = 'table_columns'
+
+    database_expression = Column(String(255))
+    expression = Column(Text)
+    python_date_format = Column(String(255))
+
+
+def upgrade():
+    bind = op.get_bind()
+    session = db.Session(bind=bind)
+
+    tables = [
+        Annotation,
+        Dashboard,
+        Database,
+        DruidCluster,
+        DruidColumn,
+        DruidDatasource,
+        DruidMetric,
+        Slice,
+        SqlaTable,
+        SqlMetric,
+        TableColumn,
+    ]
+
+    for table in tables:
+        for record in session.query(table).all():
+            for col in record.__table__.columns.values():
+                if not col.primary_key:
+                    value = getattr(record, col.name)
+
+                    if value is not None and value.strip() == '':
+                        setattr(record, col.name, None)
+
+        session.commit()
+
+    session.close()
+
+
+def downgrade():
+    pass
diff --git a/superset/views/base.py b/superset/views/base.py
index 071f2b3..113b9c6 100644
--- a/superset/views/base.py
+++ b/superset/views/base.py
@@ -19,16 +19,20 @@ from datetime import datetime
 import functools
 import logging
 import traceback
+from typing import Any, Dict
 
 from flask import abort, flash, g, get_flashed_messages, redirect, Response
 from flask_appbuilder import BaseView, 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
 from flask_babel import gettext as __
 from flask_babel import lazy_gettext as _
+from flask_wtf.form import FlaskForm
 import simplejson as json
+from wtforms.fields.core import Field, UnboundField
 import yaml
 
 from superset import conf, db, get_feature_flags, security_manager
@@ -368,3 +372,26 @@ def check_ownership(obj, raise_if_false=True):
         raise security_exception
     else:
         return False
+
+
+def bind_field(
+        self,
+        form: DynamicForm,
+        unbound_field: UnboundField,
+        options: Dict[Any, Any],
+    ) -> Field:
+    """
+    Customize how fields are bound by stripping all whitespace.
+
+    :param form: The form
+    :param unbound_field: The unbound field
+    :param options: The field options
+    :returns: The bound field
+    """
+
+    filters = unbound_field.kwargs.get('filters', [])
+    filters.append(lambda x: x.strip() if isinstance(x, str) else x)
+    return unbound_field.bind(form=form, filters=filters, **options)
+
+
+FlaskForm.Meta.bind_field = bind_field
diff --git a/tox.ini b/tox.ini
index 5eb9db5..dbe2baa 100644
--- a/tox.ini
+++ b/tox.ini
@@ -29,6 +29,8 @@ exclude =
     superset/templates
     venv
 ignore =
+    E121
+    E125
     FI12
     FI15
     FI16

Reply via email to