Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions mathesar/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
urlpatterns = [
path('api/rpc/v0/', views.MathesarRPCEntryPoint.as_view()),
path('api/db/v0/', include(db_router.urls)),
path('api/export/v0/explorations/', views.export.export_exploration, name='export_exploration'),
path('api/export/v0/tables/', views.export.export_table, name='export_table'),
path('complete_installation/', installation_incomplete(CompleteInstallationFormView.as_view()), name='complete_installation'),
path('auth/password_reset_confirm/', MathesarPasswordResetConfirmView.as_view(), name='password_reset_confirm'),
Expand Down
17 changes: 17 additions & 0 deletions mathesar/utils/explorations.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,23 @@ def run_saved_exploration(exp_model, limit, offset, conn):
return run_exploration(exploration_def, conn, limit, offset)


def exploration_chunker(
conn,
exploration_id,
limit=None,
offset=None,
batch_size=2000
):
limit = min(limit or 50000, 50000) # We cap limit at 50000
# so that we can avoid loading explorations > 50000 rows into memory.
exp_model = get_exploration(exploration_id)
exp_results = run_saved_exploration(exp_model, limit, offset, conn)
yield exp_results["output_columns"]
records = exp_results["records"]
for i in range(0, records["count"], batch_size):
yield records["results"][i:i + batch_size]


def _get_exploration_column_metadata(
exploration_def,
processed_initial_columns,
Expand Down
72 changes: 72 additions & 0 deletions mathesar/views/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,91 @@
from django import forms
from django.http import StreamingHttpResponse, JsonResponse

from mathesar.utils.explorations import exploration_chunker
from mathesar.rpc.utils import connect
from mathesar.rpc.records import Filter, OrderBy

from db.tables import fetch_table_in_chunks


class ExportExplorationQueryForm(forms.Form):
database_id = forms.IntegerField(required=True)
exploration_id = forms.IntegerField(required=True)
limit = forms.IntegerField(required=False)
offset = forms.IntegerField(required=False)


class ExportTableQueryForm(forms.Form):
database_id = forms.IntegerField(required=True)
table_oid = forms.IntegerField(required=True)
filter = forms.JSONField(required=False)
order = forms.JSONField(required=False)


def export_exploration_csv_in_chunks(
user,
database_id: int,
exploration_id: int,
**kwargs
):
with connect(database_id, user) as conn:
csv_buffer = StringIO()
exploration_chunk_gen = exploration_chunker(conn, exploration_id, **kwargs)
columns = next(exploration_chunk_gen)
csv_writer = csv.DictWriter(csv_buffer, fieldnames=columns)

csv_writer.writeheader() # writes column name(s) to csv.
value = csv_buffer.getvalue()
yield value
csv_buffer.seek(0)
csv_buffer.truncate(0)

for records in exploration_chunk_gen:
csv_writer.writerows(records)
value = csv_buffer.getvalue()
yield value
csv_buffer.seek(0)
csv_buffer.truncate(0)


def stream_exploration_as_csv(
request,
database_id: int,
exploration_id: int,
limit: int,
offset: int
) -> StreamingHttpResponse:
user = request.user
response = StreamingHttpResponse(
export_exploration_csv_in_chunks(
user,
database_id,
exploration_id,
limit=limit,
offset=offset
),
content_type="text/csv"
)
response['Content-Disposition'] = 'attachment'
return response


@login_required
def export_exploration(request):
form = ExportExplorationQueryForm(request.GET)
if form.is_valid():
data = form.cleaned_data
return stream_exploration_as_csv(
request=request,
database_id=data['database_id'],
exploration_id=data['exploration_id'],
limit=data['limit'],
offset=data['offset']
)
else:
return JsonResponse({'errors': form.errors}, status=400)


def export_table_csv_in_chunks(
user,
database_id: int,
Expand Down
5 changes: 4 additions & 1 deletion mathesar_ui/src/i18n/languages/en/dict.json
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,10 @@
"explore_your_data": "Explore your Data",
"exploring_from": "Exploring from",
"export": "Export",
"export_csv_help": "Export the {tableName} table as a CSV file. Your current filters and sorting will be applied to the exported data.",
"export_exploration_50K_limit": "Exports are limited to 50,000 rows. Since your dataset is larger, only the first 50,000 rows will be exported.",
"export_exploration_as_csv_help": "Export the {explorationName} exploration as a CSV file. Your current transformations will be applied to the exported data.",
"export_exploration_save_help": "Exploration has unsaved changes. Please save the changes before exporting.",
"export_table_as_csv_help": "Export the {tableName} table as a CSV file. Your current filters and sorting will be applied to the exported data.",
"extract_columns_to_new_table": "{count, plural, one {Extract Column Into a New Table} other {Extract Columns Into a New Table}}",
"failed_load_preview": "Failed to load preview",
"failed_to_fetch_column_information": "Failed to fetch column information.",
Expand Down
2 changes: 1 addition & 1 deletion mathesar_ui/src/i18n/languages/ja/dict.json
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@
"explore_your_data": "あなたのデータを調べる",
"exploring_from": "以下から調べる:",
"export": "エクスポート",
"export_csv_help": "{tableName} テーブルを CSVファイルとしてエクスポートしてください。現在のフィルターとソート設定が、エクスポートされたデータに適用されます。",
"export_table_as_csv_help": "{tableName} テーブルを CSVファイルとしてエクスポートしてください。現在のフィルターとソート設定が、エクスポートされたデータに適用されます。",
"extract_columns_to_new_table": "{count, plural, other {新しいテーブルに列を抽出}}",
"failed_load_preview": "プレビューの読み込みに失敗しました",
"failed_to_fetch_column_information": "列情報の取得に失敗しました。",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
<script lang="ts">
import { _ } from 'svelte-i18n';

import { getQueryStringFromParams } from '@mathesar/api/rest/utils/requestUtils';
import EntityPageHeader from '@mathesar/components/EntityPageHeader.svelte';
import InspectorButton from '@mathesar/components/InspectorButton.svelte';
import NameAndDescInputModalForm from '@mathesar/components/NameAndDescInputModalForm.svelte';
import SaveButton from '@mathesar/components/SaveButton.svelte';
import SelectTableWithinCurrentSchema from '@mathesar/components/SelectTableWithinCurrentSchema.svelte';
import TableName from '@mathesar/components/TableName.svelte';
import { iconExploration } from '@mathesar/icons';
import { iconExploration, iconExport } from '@mathesar/icons';
import type { Table } from '@mathesar/models/Table';
import { modal } from '@mathesar/stores/modal';
import { queries } from '@mathesar/stores/queries';
import { currentTablesData as tablesDataStore } from '@mathesar/stores/tables';
import { toast } from '@mathesar/stores/toast';
import { Button, Help } from '@mathesar-component-library';
import {
AnchorButton,
Button,
Help,
Icon,
Tooltip,
} from '@mathesar-component-library';

import type QueryManager from '../QueryManager';
import type { ColumnWithLink } from '../utils';
Expand All @@ -25,13 +32,17 @@
{};
export let isInspectorOpen: boolean;

$: ({ query, queryHasUnsavedChanges } = queryManager);
$: ({ rowsData, query, queryHasUnsavedChanges } = queryManager);
$: currentTable = $query.base_table_oid
? $tablesDataStore.tablesMap.get($query.base_table_oid)
: undefined;
$: isSaved = $query.isSaved();
$: hasColumns = $query.initial_columns.length > 0;
$: canSave = !!$query.base_table_oid && hasColumns && $queryHasUnsavedChanges;
$: exportLinkParams = getQueryStringFromParams({
database_id: $query.database_id,
exploration_id: $query.id,
});

function updateBaseTable(table: Table | undefined) {
void queryManager.update((q) =>
Expand Down Expand Up @@ -134,6 +145,48 @@
unsavedChangesText={$_('exploration_has_unsaved_changes')}
onSave={saveExistingOrCreateNew}
/>

{#if hasColumns && !canSave}
<Tooltip allowHover>
<AnchorButton
slot="trigger"
href="/api/export/v0/explorations/?{exportLinkParams}"
data-tinro-ignore
appearance="secondary"
size="medium"
aria-label={$_('export')}
download="{$query.name}.csv"
>
<Icon {...iconExport} />
<span class="responsive-button-label">{$_('export')}</span>
</AnchorButton>
<span slot="content">
{$_('export_exploration_as_csv_help', {
values: { explorationName: $query.name },
})}
{#if $rowsData.totalCount > 50000}
{$_('export_exploration_50K_limit')}
{/if}
</span>
</Tooltip>
{:else}
<Tooltip enabled={hasColumns}>
<Button
slot="trigger"
appearance="secondary"
size="medium"
aria-label={$_('export')}
disabled="true"
>
<Icon {...iconExport} />
<span class="responsive-button-label">{$_('export')}</span>
</Button>
<span slot="content">
{$_('export_exploration_save_help')}
</span>
</Tooltip>
{/if}

<InspectorButton
disabled={!hasColumns}
active={isInspectorOpen}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@
<span class="responsive-button-label">{$_('export')}</span>
</AnchorButton>
<span slot="content">
{$_('export_csv_help', {
{$_('export_table_as_csv_help', {
values: { tableName: table.name },
})}
</span>
Expand Down
Loading