diff --git a/HISTORY.rst b/HISTORY.rst index 227dac06..238372c3 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -5,17 +5,19 @@ Change Log This document records all notable changes to `django-sql-explorer `_. This project adheres to `Semantic Versioning `_. -`5.0.0b1`_ (2024-06-07) +`5.0.b2`_ (2024-06-17) =========================== -* Manage DB connections via the UI (and/or Django Admin) +* Manage DB connections via the UI (and/or Django Admin). Set EXPLORER_DB_CONNECTIONS_ENABLED + to True in settings to enable user-facing connection management. * Upload CSV or SQLite DBs directly, to create additional connections. This functionality has additional dependencies which can be installed with - the 'uploads' extra (e.g. pip install django-sql-explorer[uploads]) + the 'uploads' extra (e.g. pip install django-sql-explorer[uploads]). Then set EXPLORER_USER_UPLOADS_ENABLED + to True, and make sure S3_BUCKET is also set up. * The above functionality is managed by a new license, restricting the ability of 3rd parties resell SQL Explorer (commercial usage is absolutely still permitted). -* Query List home page is sortable. +* Query List home page is sortable * Select all / deselect all with AI assistant * Assistant tests run reliably in CI/CD * Introduced some branding and styling improvements diff --git a/explorer/__init__.py b/explorer/__init__.py index 57bb0fcf..abbb9748 100644 --- a/explorer/__init__.py +++ b/explorer/__init__.py @@ -3,7 +3,7 @@ "minor": 0, "patch": 0, "releaselevel": "beta", - "serial": 1 + "serial": 2 } diff --git a/explorer/app_settings.py b/explorer/app_settings.py index ca432fb6..c558e3f9 100644 --- a/explorer/app_settings.py +++ b/explorer/app_settings.py @@ -157,10 +157,22 @@ "max_tokens": 128000}) EXPLORER_DB_CONNECTIONS_ENABLED = getattr(settings, "EXPLORER_DB_CONNECTIONS_ENABLED", False) +EXPLORER_USER_UPLOADS_ENABLED = getattr(settings, "EXPLORER_USER_UPLOADS_ENABLED", False) EXPLORER_PRUNE_LOCAL_UPLOAD_COPY_DAYS_INACTIVITY = getattr(settings, "EXPLORER_PRUNE_LOCAL_UPLOAD_COPY_DAYS_INACTIVITY", 7) # 500mb default max EXPLORER_MAX_UPLOAD_SIZE = getattr(settings, "EXPLORER_MAX_UPLOAD_SIZE", 500 * 1024 * 1024) -def has_assistant(): return EXPLORER_AI_API_KEY is not None -def db_connections_enabled(): return EXPLORER_DB_CONNECTIONS_ENABLED + +def has_assistant(): + return EXPLORER_AI_API_KEY is not None + + +def db_connections_enabled(): + return EXPLORER_DB_CONNECTIONS_ENABLED + + +def user_uploads_enabled(): + return (EXPLORER_USER_UPLOADS_ENABLED and + EXPLORER_DB_CONNECTIONS_ENABLED and + S3_BUCKET is not None) diff --git a/explorer/ee/db_connections/utils.py b/explorer/ee/db_connections/utils.py index c43a3d98..cda912a0 100644 --- a/explorer/ee/db_connections/utils.py +++ b/explorer/ee/db_connections/utils.py @@ -2,7 +2,6 @@ from django.db.utils import load_backend import os -import locale from dateutil import parser import pandas as pd @@ -112,12 +111,25 @@ def pandas_to_sqlite(df, local_path="local_database.db"): SHORTEST_PLAUSIBLE_DATE_STRING = 5 +def atof_custom(value): + # Remove any thousands separators and convert the decimal point + if "," in value and "." in value: + if value.index(",") < value.index("."): + # 0,000.00 format + value = value.replace(",", "") + else: + # 0.000,00 format + value = value.replace(".", "").replace(",", ".") + elif "," in value: + # No decimal point, only thousands separator + value = value.replace(",", "") + return float(value) + def csv_to_typed_df(csv_bytes, delimiter=",", has_headers=True): # noqa try: csv_file = io.BytesIO(csv_bytes) df = pd.read_csv(csv_file, sep=delimiter, header=0 if has_headers else None) - locale.setlocale(locale.LC_NUMERIC, "en_US.UTF-8") for column in df.columns: values = df[column].dropna().unique() @@ -130,7 +142,7 @@ def csv_to_typed_df(csv_bytes, delimiter=",", has_headers=True): # noqa for value in values: try: - float_val = locale.atof(str(value)) + float_val = atof_custom(str(value)) if float_val == int(float_val): continue # This is effectively an integer else: @@ -163,12 +175,12 @@ def csv_to_typed_df(csv_bytes, delimiter=",", has_headers=True): # noqa if is_date: df[column] = pd.to_datetime(df[column], errors="coerce", utc=True) elif is_integer: - df[column] = df[column].apply(lambda x: int(locale.atof(str(x))) if pd.notna(x) else x) + df[column] = df[column].apply(lambda x: int(atof_custom(str(x))) if pd.notna(x) else x) # If there are NaN / blank values, the column will be converted to float # Convert it back to integer df[column] = df[column].astype("Int64") elif is_float: - df[column] = df[column].apply(lambda x: locale.atof(str(x)) if pd.notna(x) else x) + df[column] = df[column].apply(lambda x: atof_custom(str(x)) if pd.notna(x) else x) else: inferred_type = pd.api.types.infer_dtype(values) if inferred_type == "integer": diff --git a/explorer/src/images/logo-main.svg b/explorer/src/images/logo-main.svg new file mode 100644 index 00000000..13c213aa --- /dev/null +++ b/explorer/src/images/logo-main.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/explorer/src/images/logo.png b/explorer/src/images/logo.png new file mode 100644 index 00000000..dacdef6a Binary files /dev/null and b/explorer/src/images/logo.png differ diff --git a/explorer/src/images/logo.svg b/explorer/src/images/logo.svg deleted file mode 100644 index 60407416..00000000 --- a/explorer/src/images/logo.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/explorer/src/scss/explorer.scss b/explorer/src/scss/explorer.scss index 7aedda51..69d4a738 100644 --- a/explorer/src/scss/explorer.scss +++ b/explorer/src/scss/explorer.scss @@ -108,27 +108,6 @@ div.sort { display: none; } -nav.navbar { - .nav-link { - color: white; - } - - nav.nav-link:hover, .nav-link:focus { - color: var(--bs-primary-bg-subtle); - } - - .nav-pills .nav-link.active, .nav-pills .show > .nav-link { - color: var(--bs-nav-pills-link-active-color); - border: 1px solid var(--bs-secondary-bg); - background: none; - } - - .nav-link:hover, .navbar-brand:hover { - background: none; - color: var(--bs-primary-bg-subtle) !important; - } -} - .dropdown-toggle::after { margin-left: 0 !important; } @@ -151,3 +130,11 @@ nav.navbar { .logo-image { height: 2rem; } + +.stats-wrapper { + td { + padding-top: 0; + padding-bottom: 0; + border: none; + } +} diff --git a/explorer/templates/connections/database_connection_form.html b/explorer/templates/connections/database_connection_form.html index f5ef5260..8fa6edfc 100644 --- a/explorer/templates/connections/database_connection_form.html +++ b/explorer/templates/connections/database_connection_form.html @@ -9,7 +9,7 @@

{% if object %}Edit{% else %}Create New{% endif %} Connection

Cancel - {% if not object and s3_enabled %} + {% if not object and user_uploads_enabled %}

...or upload a SQLite DB or CSV File

CSV files will get parsed and typed automatically. SQLite databases must not be password protected.

diff --git a/explorer/templates/explorer/assistant.html b/explorer/templates/explorer/assistant.html index dce8ed0e..e5b61ee2 100644 --- a/explorer/templates/explorer/assistant.html +++ b/explorer/templates/explorer/assistant.html @@ -26,7 +26,7 @@ data-bs-toggle="tooltip" data-bs-placement="top" data-bs-title="SQL Assistant builds a prompt with your query, your request, relevant schema (tables referenced in your query) and sample data from those tables. You can optionally include other tables (schema + data sample) that you'd like SQL Assistant to know about.">(?) - +
diff --git a/explorer/templates/explorer/base.html b/explorer/templates/explorer/base.html index 4f5b2201..1c622006 100644 --- a/explorer/templates/explorer/base.html +++ b/explorer/templates/explorer/base.html @@ -7,7 +7,7 @@ {% trans "SQL Explorer" %}{% if query %} - {{ query.title }}{% elif title %} - {{ title }}{% endif %} - + {% block style %} {% vite_asset 'scss/styles.scss' %} @@ -41,11 +41,11 @@

This is easy to fix, I promise!

{% endif %} {% block sql_explorer_content_takeover %} -