feat: CSV File Upload form updates (Grouping with Collapse/Expand) (#21992)

This commit is contained in:
Antonio Rivero Martinez 2022-11-30 12:03:52 -03:00 committed by GitHub
parent b1f8fd4f64
commit 2fd0a6146e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 288 additions and 7 deletions

View File

@ -0,0 +1,75 @@
{#
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.
#}
{% macro render_delimiter_field(field, begin_sep_label='', end_sep_label='', begin_sep_field='', end_sep_field='') %}
{% if field.id != 'csrf_token' %}
{% if field.type == 'HiddenField' %}
{{ field}}
{% else %}
{{begin_sep_label|safe}}
<label for="{{field.id}}" control-label>
{{ field.label.text }}
{% if field.flags.required %}
<strong style="color: red">&#42;</strong>
{% endif %}
</label>
{{end_sep_label|safe}}
{{begin_sep_field|safe}}
{{ field(**kwargs)|safe }}
<input class="form-control col-sm-9" style="margin: 10px 0px; display: none;" id="otherInput" name="otherInput" placeholder="Type your delimiter here" type="text" value="">
<span class="help-block">{{ field.description }}</span>
{% endif %}
{% if field.errors %}
<div class="alert alert-danger">
{% for error in field.errors %}
{{ _(error) }}
{% endfor %}
</div>
{% endif %}
{{end_sep_field|safe}}
{% endif %}
{% endmacro %}
{% macro render_collapsable_form_group(id, section_title='') %}
<div class="form-group" id="{{id}}">
<div class="col-xs-12" style="padding: 0;">
<table class="table table-bordered">
<tbody>
<tr data-toggle="collapse" data-target="#collapsable-content-{{id}}" class="accordion-toggle">
<td class="col-xs-12" role="button" style="border: none;">
<i class="fa fa-chevron-down" style="color: #666666; margin-right: 8px; margin-left: 12px;"></i>
{{section_title}}
</td>
</tr>
<tr class="collapse" id="collapsable-content-{{id}}">
<td colspan="12" style="padding: 0;">
<div>
<table class="table table-bordered" style="margin-bottom: 0; background-color: transparent; border: none;">
<tbody>
{{ caller() }}
</tbody>
</table>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
{% endmacro %}

View File

@ -0,0 +1,37 @@
{#
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.
#}
<script>
$('#delimiter').on('change', function () {
var delimiterOptions = $('#delimiter').val();
if (delimiterOptions?.includes("other")) {
document.getElementById("otherInput").style.display = 'block';
$('#otherInput').attr('required', 'required');
} else {
document.getElementById("otherInput").style.display = 'none';
$('#otherInput').removeAttr('required');
}
}).change();
$(".collapse").on("hide.bs.collapse show.bs.collapse", e => {
$(e.target)
.prev()
.find("i:last-child")
.toggleClass("fa-chevron-up fa-chevron-down");
});
</script>

View File

@ -16,10 +16,122 @@
specific language governing permissions and limitations
under the License.
#}
{% extends "appbuilder/base.html" %}
{% import 'appbuilder/general/lib.html' as lib %}
{% set begin_sep_label = '<td class="col-sm-2" style="border-left: 0; border-top: 0;">' %}
{% set end_sep_label = '</td>' %}
{% set begin_sep_field = '<td style="border-right: 0; border-top: 0;">' %}
{% set end_sep_field = '</td>' %}
{% import 'superset/form_view/database_schemas_selector.html' as schemas_selector %}
{% extends 'appbuilder/general/model/edit.html' %}
{% import 'superset/form_view/csv_scripts.html' as csv_scripts %}
{% import 'superset/form_view/csv_macros.html' as csv_macros %}
{% block content %}
{{ lib.panel_begin(title, "edit") }}
<div id="Home" class="tab-pane active">
<form id="model_form" action="" method="post" enctype="multipart/form-data">
{{form.hidden_tag()}}
<div class="form-group">
<div class="col-md-12" style="padding: 0;">
<table class="table table-bordered">
<tbody>
<tr>
{{ lib.render_field(form.csv_file, begin_sep_label, end_sep_label, begin_sep_field, end_sep_field) }}
</tr>
<tr>
{{ lib.render_field(form.table_name, begin_sep_label, end_sep_label, begin_sep_field, end_sep_field) }}
</tr>
<tr>
{{ lib.render_field(form.database, begin_sep_label, end_sep_label, begin_sep_field, end_sep_field) }}
</tr>
<tr>
{{ lib.render_field(form.schema, begin_sep_label, end_sep_label, begin_sep_field, end_sep_field) }}
</tr>
<tr>
{{ csv_macros.render_delimiter_field(form.delimiter, begin_sep_label, end_sep_label, begin_sep_field, end_sep_field) }}
</tr>
</tbody>
</table>
</div>
</div>
{% call csv_macros.render_collapsable_form_group("accordion1", "File Settings") %}
<tr>
{{ lib.render_field(form.if_exists, begin_sep_label, end_sep_label, begin_sep_field,
end_sep_field) }}
</tr>
<tr>
{{ lib.render_field(form.skip_initial_space, begin_sep_label, end_sep_label, begin_sep_field,
end_sep_field) }}
</tr>
<tr>
{{ lib.render_field(form.skip_blank_lines, begin_sep_label, end_sep_label, begin_sep_field,
end_sep_field) }}
</tr>
<tr>
{{ lib.render_field(form.parse_dates, begin_sep_label, end_sep_label, begin_sep_field,
end_sep_field) }}
</tr>
<tr>
{{ lib.render_field(form.infer_datetime_format, begin_sep_label, end_sep_label, begin_sep_field,
end_sep_field) }}
</tr>
<tr>
{{ lib.render_field(form.decimal, begin_sep_label, end_sep_label, begin_sep_field,
end_sep_field) }}
</tr>
<tr>
{{ lib.render_field(form.null_values, begin_sep_label, end_sep_label, begin_sep_field,
end_sep_field) }}
</tr>
{% endcall %}
{% call csv_macros.render_collapsable_form_group("accordion2", "Columns") %}
<tr>
{{ lib.render_field(form.index_col, begin_sep_label, end_sep_label, begin_sep_field,
end_sep_field) }}
</tr>
<tr>
{{ lib.render_field(form.dataframe_index, begin_sep_label, end_sep_label, begin_sep_field,
end_sep_field) }}
</tr>
<tr>
{{ lib.render_field(form.index_label, begin_sep_label, end_sep_label, begin_sep_field,
end_sep_field) }}
</tr>
<tr>
{{ lib.render_field(form.use_cols, begin_sep_label, end_sep_label, begin_sep_field,
end_sep_field) }}
</tr>
<tr>
{{ lib.render_field(form.overwrite_duplicate, begin_sep_label, end_sep_label, begin_sep_field,
end_sep_field) }}
</tr>
{% endcall %}
{% call csv_macros.render_collapsable_form_group("accordion3", "Rows") %}
<tr>
{{ lib.render_field(form.header, begin_sep_label, end_sep_label, begin_sep_field, end_sep_field)
}}
</tr>
<tr>
{{ lib.render_field(form.nrows, begin_sep_label, end_sep_label, begin_sep_field, end_sep_field)
}}
</tr>
<tr>
{{ lib.render_field(form.skiprows, begin_sep_label, end_sep_label, begin_sep_field,
end_sep_field) }}
</tr>
{% endcall %}
<div class="form-group">
<div class="col-xs-12" style="padding: 0;">
{{ lib.render_form_controls() }}
</div>
</div>
</form>
</div>
{% endblock %}
{% block add_tail_js %}
<script src="{{url_for('appbuilder.static',filename='js/ab_keep_tab.js')}}"></script>
{% endblock %}
{% block tail_js %}
{{ super() }}
{{ schemas_selector }}
{{ csv_scripts }}
{% endblock %}

View File

@ -146,11 +146,19 @@ class CsvToDatabaseForm(UploadToDatabaseForm):
validators=[Optional()],
widget=BS3TextFieldWidget(),
)
delimiter = StringField(
delimiter = SelectField(
_("Delimiter"),
description=_("Enter a delimiter for this data"),
choices=[
(",", _(",")),
(".", _(".")),
("other", _("Other")),
],
validators=[DataRequired()],
widget=BS3TextFieldWidget(),
default=[","],
)
otherInput = StringField(
_("Other"),
)
if_exists = SelectField(
_("If Table Already Exists"),

View File

@ -18,7 +18,7 @@ import io
import os
import tempfile
import zipfile
from typing import TYPE_CHECKING
from typing import Any, TYPE_CHECKING
import pandas as pd
from flask import flash, g, redirect
@ -109,7 +109,52 @@ class DatabaseView(
return super().render_app_template()
class CsvToDatabaseView(SimpleFormView):
class CustomFormView(SimpleFormView):
"""
View for presenting your own forms
Inherit from this view to provide some base
processing for your customized form views.
Notice that this class inherits from BaseView
so all properties from the parent class can be overridden also.
Implement form_get and form_post to implement
your form pre-processing and post-processing
"""
@expose("/form", methods=["GET"])
@has_access
def this_form_get(self) -> Any:
self._init_vars()
form = self.form.refresh()
self.form_get(form)
self.update_redirect()
return self.render_template(
self.form_template,
title=self.form_title,
form=form,
appbuilder=self.appbuilder,
)
@expose("/form", methods=["POST"])
@has_access
def this_form_post(self) -> Any:
self._init_vars()
form = self.form.refresh()
if form.validate_on_submit():
response = self.form_post(form) # pylint: disable=assignment-from-no-return
if not response:
return redirect(self.get_redirect())
return response
return self.render_template(
self.form_template,
title=self.form_title,
form=form,
appbuilder=self.appbuilder,
)
class CsvToDatabaseView(CustomFormView):
form = CsvToDatabaseForm
form_template = "superset/form_view/csv_to_database_view/edit.html"
form_title = _("CSV to Database configuration")
@ -128,6 +173,7 @@ class CsvToDatabaseView(SimpleFormView):
def form_post(self, form: CsvToDatabaseForm) -> Response:
database = form.database.data
csv_table = Table(table=form.table_name.data, schema=form.schema.data)
delimiter_input = form.delimiter.data
if not schema_allows_file_upload(database, csv_table.schema):
message = __(
@ -139,6 +185,9 @@ class CsvToDatabaseView(SimpleFormView):
flash(message, "danger")
return redirect("/csvtodatabaseview/form")
if form.delimiter.data == "other":
delimiter_input = form.otherInput.data
try:
df = pd.concat(
pd.read_csv(
@ -155,7 +204,7 @@ class CsvToDatabaseView(SimpleFormView):
na_values=form.null_values.data if form.null_values.data else None,
nrows=form.nrows.data,
parse_dates=form.parse_dates.data,
sep=form.delimiter.data,
sep=delimiter_input,
skip_blank_lines=form.skip_blank_lines.data,
skipinitialspace=form.skip_initial_space.data,
skiprows=form.skiprows.data,