diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index e7b4a360..cd669d8f 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -21,6 +21,14 @@ jobs: with: python-version: '3.12' + - uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + - name: Install dependencies + run: npm install + - name: Build client + run: npm run build + - name: Install pypa/build run: >- python -m diff --git a/.github/workflows/publish-test.yml b/.github/workflows/publish-test.yml index 6dbea01a..9fc6891b 100644 --- a/.github/workflows/publish-test.yml +++ b/.github/workflows/publish-test.yml @@ -28,6 +28,14 @@ jobs: pip install build --user + - uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + - name: Install dependencies + run: npm install + - name: Build client + run: npm run build + - name: Build a binary wheel and a source tarball run: >- python -m diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ccfad331..ffe25ee7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,6 +31,10 @@ jobs: python-version: ${{ matrix.python-version }} cache: pip + - uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + - name: Install dependencies run: | npm install diff --git a/HISTORY.rst b/HISTORY.rst index 39ddc4a3..af115045 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -5,12 +5,13 @@ Change Log This document records all notable changes to `django-sql-explorer `_. This project adheres to `Semantic Versioning `_. -`unreleased`_ changes +`4.0.0.beta1`_ (2024-01-31) --------------------- -* `#565`_: Front-end modernization. Vite, Boostrap 5, CodeMirror 6, etc. +* `#565`_: Front-end modernization. Code completion via CodeMirror 6. Bootstrap5. Vite-based build * `#566`_: Django 5 support & tests * `#537`_: S3 signature version support * `#562`_: Visually show whether the last run was successful +* `#571`_: Replace isort and flake8 with Ruff (linting) `3.2.1`_ (2023-07-13) diff --git a/MANIFEST.in b/MANIFEST.in index 5eaed2f7..9bfa38e8 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,5 @@ recursive-include explorer * recursive-exclude * *.pyc __pycache__ .DS_Store +include package.json +include vite.config.js include README.rst diff --git a/docs/install.rst b/docs/install.rst index 09659251..b535b7e1 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -1,7 +1,7 @@ Install ======= -* Requires Python 3.6 or higher. +* Requires Python 3.10 or higher. * Requires Django 3.2 or higher. Set up a Django project with the following: @@ -36,21 +36,6 @@ Add to your ``INSTALLED_APPS``, located in the ``settings.py`` file in your proj ... ) -Add the following to your urls.py (all Explorer URLs are restricted -via the ``EXPLORER_PERMISSION_VIEW`` and ``EXPLORER_PERMISSION_CHANGE`` -settings. See Settings section below for further documentation.): - -.. code-block:: python - :emphasize-lines: 5 - - from django.urls import path - - urlpatterns = [ - ... - path('explorer/', include('explorer.urls')), - ... - ] - Configure your settings to something like: .. code-block:: python @@ -65,6 +50,21 @@ Explorer users, and the values are the actual database aliases used in in your database, add them in your project's ``DATABASES`` setting and use these read-only connections in the ``EXPLORER_CONNECTIONS``. +Add the following to your urls.py (all Explorer URLs are restricted +via the ``EXPLORER_PERMISSION_VIEW`` and ``EXPLORER_PERMISSION_CHANGE`` +settings. See Settings section below for further documentation.): + +.. code-block:: python + :emphasize-lines: 5 + + from django.urls import path, include + + urlpatterns = [ + ... + path('explorer/', include('explorer.urls')), + ... + ] + If you want to quickly use django-sql-explorer with the existing default connection **and know what you are doing** (or you are on development), you can use the following settings: @@ -74,11 +74,19 @@ can use the following settings: EXPLORER_CONNECTIONS = { 'Default': 'default' } EXPLORER_DEFAULT_CONNECTION = 'default' -Finally, run migrate to create the tables: +Run migrate to create the tables: ``python manage.py migrate`` -You can now browse to https://yoursite/explorer/ and get exploring! +Create a superuser: + +``python manage.py createsuperuser`` + +And run the server: + +``python manage.py runserver`` + +You can now browse to http://127.0.0.1:8000/explorer/ and get exploring! The default behavior when viewing a parameterized query is to autorun the associated SQL with the default parameter values. This may perform poorly and you may want @@ -94,3 +102,33 @@ There are a handful of features (snapshots, emailing queries) that rely on Celery and the dependencies in optional-requirements.txt. If you have Celery installed, set ``EXPLORER_TASKS_ENABLED=True`` in your settings.py to enable these features. + +Installing From Source +---------------------- + +If you are installing SQL Explorer from source (by cloning the repository), +you may want to first look at simply running test_project/start.sh. + +If you want to install it into an existing project, you can do so by following +the instructions above, and additionally building the front-end dependencies. + +After cloning, simply run: + +:: + + nvm install + nvm use + npm install + npm run build + +The front-end assets will be built and placed in the /static/ folder +and collected properly by your Django installation during the `collect static` +phase. Copy the /explorer directory into site-packages and you're ready to go. + +And frankly, as long as you have a reasonably modern version of Node and NPM +installed, you can probably skip the nvm steps. + +Because the front-end assets must be built, installing SQL Explorer via pip +from github is not supported. The package will be installed, but the front-end +assets will be missing and will not be able to be built, as the necessary +configuration files are not included when github builds the wheel for pip. diff --git a/explorer/__init__.py b/explorer/__init__.py index 0725644c..40b2bffd 100644 --- a/explorer/__init__.py +++ b/explorer/__init__.py @@ -1,9 +1,9 @@ __version_info__ = { - "major": 3, - "minor": 2, - "patch": 1, - "releaselevel": "final", - "serial": 0 + "major": 4, + "minor": 0, + "patch": 0, + "releaselevel": "beta", + "serial": 1 } diff --git a/explorer/app_settings.py b/explorer/app_settings.py index 041363f8..99aaab23 100644 --- a/explorer/app_settings.py +++ b/explorer/app_settings.py @@ -146,3 +146,4 @@ # If set to False will not autorun queries containing parameters when viewed # - user will need to run by clicking the Save & Run Button to execute EXPLORER_AUTORUN_QUERY_WITH_PARAMS = getattr(settings, "EXPLORER_AUTORUN_QUERY_WITH_PARAMS", True) +VITE_DEV_MODE = getattr(settings, "VITE_DEV_MODE", False) diff --git a/explorer/src/scss/explorer.scss b/explorer/src/scss/explorer.scss index 8056a21b..caaaed6a 100644 --- a/explorer/src/scss/explorer.scss +++ b/explorer/src/scss/explorer.scss @@ -3,6 +3,10 @@ border: 1px solid silver } +.cm-editor { + outline: none !important; +} + .cm-scroller { overflow: auto; min-height: 350px @@ -95,9 +99,28 @@ div.sort { } .btn-save-only { - border-radius: 0px !important; + border-radius: 0 !important; +} + +.vite-not-running-canary { + display: none; +} + +.nav-link { + color: white; +} + +.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; } -.dropdown-toggle::after{ - margin-left: 0px !important; +.navbar .navbar-brand:hover { + background: none; + color: var(--bs-primary-bg-subtle) !important; } diff --git a/explorer/src/scss/main.scss b/explorer/src/scss/main.scss index 933e037f..4881de73 100644 --- a/explorer/src/scss/main.scss +++ b/explorer/src/scss/main.scss @@ -4,6 +4,7 @@ $bootstrap-icons-font-dir: "../../../node_modules/bootstrap-icons/font/fonts"; @import "~bootstrap-icons/font/bootstrap-icons.css"; +@import "variables"; @import "explorer"; @import "pivot.css"; diff --git a/explorer/src/scss/variables.scss b/explorer/src/scss/variables.scss new file mode 100644 index 00000000..708f726a --- /dev/null +++ b/explorer/src/scss/variables.scss @@ -0,0 +1,3 @@ +:root { + --bs-dark-rgb: 44, 62, 80; +} diff --git a/explorer/templates/explorer/base.html b/explorer/templates/explorer/base.html index e2bc2a9f..24312098 100644 --- a/explorer/templates/explorer/base.html +++ b/explorer/templates/explorer/base.html @@ -20,36 +20,55 @@ +{% if vite_dev_mode %} +
+
+

Looks like Vite isn't running

+

This is easy to fix, I promise!

+
You can run:
+
+npm run dev
+            
+
Then refresh this page, and you'll get all of your styles, JS, and hot-reloading!
+
If this is the first time you are running the project, then run:
+
+nvm install
+nvm use
+npm install
+npm run dev
+        
+
+
+{% endif %} + {% block sql_explorer_content_takeover %} - +{% block sql_explorer_content %}{% endblock %} {% endblock %} {% block sql_explorer_footer %}
diff --git a/explorer/templates/explorer/play.html b/explorer/templates/explorer/play.html index a6c28e20..21537798 100644 --- a/explorer/templates/explorer/play.html +++ b/explorer/templates/explorer/play.html @@ -5,71 +5,73 @@ -
-
-

{% trans "Playground" %}

-

- {% blocktrans trimmed %} - The playground is for experimenting and writing ad-hoc queries. By default, nothing you do here will be saved. - {% endblocktrans %} -

-
{% csrf_token %} - {% if error %} -
{{ error|escape }}
- {% endif %} - {{ form.non_field_errors }} - {% if form.connections|length > 1 and can_change %} -
- {{ form.connection }} - +
+
+
+

{% trans "Playground" %}

+

+ {% blocktrans trimmed %} + The playground is for experimenting and writing ad-hoc queries. By default, nothing you do here will be saved. + {% endblocktrans %} +

+ {% csrf_token %} + {% if error %} +
{{ error|escape }}
+ {% endif %} + {{ form.non_field_errors }} + {% if form.connections|length > 1 and can_change %} +
+ {{ form.connection }} + +
+ {% else %} + {# still need to submit the connection, just hide the UI element #} + + {% endif %} +
+
+ +
+
+ {% if ql_id %} + + + + {% endif %} +
- {% else %} - {# still need to submit the connection, just hide the UI element #} -
{% include 'explorer/preview_pane.html' %} diff --git a/explorer/templates/explorer/preview_pane.html b/explorer/templates/explorer/preview_pane.html index 1f6a9140..15d66664 100644 --- a/explorer/templates/explorer/preview_pane.html +++ b/explorer/templates/explorer/preview_pane.html @@ -62,24 +62,26 @@ data-dir="asc">{{ h }} {% endfor %} - - - {% for h in headers %} - - {% if h.summary %} - - - {% for label, value in h.summary.stats.items %} - - - - - {% endfor %} -
{{ label }}{{ value }}
- {% endif %} - - {% endfor %} - + {% if has_stats %} + + + {% for h in headers %} + + {% if h.summary %} + + + {% for label, value in h.summary.stats.items %} + + + + + {% endfor %} +
{{ label }}{{ value }}
+ {% endif %} + + {% endfor %} + + {% endif %} {% if data %} diff --git a/explorer/templates/explorer/query.html b/explorer/templates/explorer/query.html index ed0b6ea5..0d6211ed 100644 --- a/explorer/templates/explorer/query.html +++ b/explorer/templates/explorer/query.html @@ -7,134 +7,132 @@
-
- {% if query %} - {% query_favorite_button query.id is_favorite 'query_favorite_toggle query_favourite_detail'%} - {% endif %} -

+
+
{% if query %} - {{ query.title }} - {% else %} - {% trans "New Query" %} - {% endif %} -

- {% if shared %}  shared{% endif %} - {% if message %} -
{{ message }}
- {% endif %} -
- {% if query %} -
{% csrf_token %} - {% else %} - {% csrf_token %} - {% endif %} - {% if error %} -
{{ error|escape }}
+ {% query_favorite_button query.id is_favorite 'query_favorite_toggle query_favourite_detail'%} {% endif %} - {{ form.non_field_errors }} -
- {% if form.title.errors %}{% for error in form.title.errors %} -
{{ error|escape }}
- {% endfor %}{% endif %} - - -
- {% if form.connections|length > 1 and can_change %} -
- {{ form.connection }} - -
- {% else %} - {# still need to submit the connection, just hide the UI element #} - +

+ {% if query %} + {{ query.title }} + {% else %} + {% trans "New Query" %} + {% endif %} +

+ {% if shared %}  shared{% endif %} + {% if message %} +
{{ message }}
{% endif %} -
- {% if form.description.errors %} -
{{ form.description.errors }}
+
+ {% if query %} + {% csrf_token %} + {% else %} + {% csrf_token %} {% endif %} - - -
- - {% if form.sql.errors %} - {% for error in form.sql.errors %} + {% if error %}
{{ error|escape }}
- {% endfor %} - {% endif %} -
-
-
- + {% endif %} + {{ form.non_field_errors }} +
+ {% if form.title.errors %}{% for error in form.title.errors %} +
{{ error|escape }}
+ {% endfor %}{% endif %} + + +
+ {% if form.connections|length > 1 and can_change %} +
+ {{ form.connection }} +
+ {% else %} + {# still need to submit the connection, just hide the UI element #} + + {% endif %} +
+ {% if form.description.errors %} +
{{ form.description.errors }}
+ {% endif %} + +
-
-
-
- + + {% if form.sql.errors %} + {% for error in form.sql.errors %} +
{{ error|escape }}
+ {% endfor %} + {% endif %} +
+
+
+
- {% if params %} +
+
+
- {% include 'explorer/params.html' %} +
- {% endif %} + {% if params %} +
+ {% include 'explorer/params.html' %} +
+ {% endif %} +
-
-
- {% if query %} -
- - - - - -
- {% endif %} -
- {% if can_change %} - - - - {% export_buttons query %} - - - - {% else %} - - {% export_buttons query %} +
+ {% if query %} +
+ + + + + +
{% endif %} +
+ {% if can_change %} + + + {% export_buttons query %} + + + + {% else %} + + {% export_buttons query %} + {% endif %} +
+
-
-
-
diff --git a/explorer/templatetags/vite.py b/explorer/templatetags/vite.py index b94630b8..3da466cc 100644 --- a/explorer/templatetags/vite.py +++ b/explorer/templatetags/vite.py @@ -5,6 +5,7 @@ from django import template from django.conf import settings from django.utils.safestring import mark_safe +from explorer import app_settings register = template.Library() @@ -12,13 +13,12 @@ VITE_OUTPUT_DIR = "/static/explorer/" VITE_DEV_DIR = "explorer/src/" VITE_MANIFEST_FILE = os.path.join(os.path.dirname(__file__), "../static/explorer/.vite/manifest.json") -VITE_DEV_MODE = getattr(settings, "VITE_DEV_MODE", False) VITE_SERVER_HOST = getattr(settings, "VITE_SERVER_HOST", "localhost") VITE_SERVER_PORT = getattr(settings, "VITE_SERVER_PORT", "5173") def get_css_link(file: str) -> str: - if VITE_DEV_MODE is False: + if app_settings.VITE_DEV_MODE is False: base_url = f"{VITE_OUTPUT_DIR}" else: base_url = f"http://{VITE_SERVER_HOST}:{VITE_SERVER_PORT}/{VITE_DEV_DIR}" @@ -26,7 +26,7 @@ def get_css_link(file: str) -> str: def get_script(file: str) -> str: - if VITE_DEV_MODE is False: + if app_settings.VITE_DEV_MODE is False: return mark_safe(f'') # nosec B308, B703 else: base_url = f"http://{VITE_SERVER_HOST}:{VITE_SERVER_PORT}/{VITE_DEV_DIR}" @@ -44,7 +44,7 @@ def get_manifest(): @register.simple_tag def vite_asset(filename: str): is_css = str(filename).endswith("css") - if VITE_DEV_MODE is True: + if app_settings.VITE_DEV_MODE is True: if is_css is True: return get_css_link(filename) return get_script(filename) @@ -62,7 +62,7 @@ def vite_asset(filename: str): @register.simple_tag def vite_hmr_client(): - if VITE_DEV_MODE is False: + if app_settings.VITE_DEV_MODE is False: return "" base_url = f"http://{VITE_SERVER_HOST}:{VITE_SERVER_PORT}/@vite/client" return mark_safe(f'') # nosec B308, B703 diff --git a/explorer/views/list.py b/explorer/views/list.py index 7282a910..456a251a 100644 --- a/explorer/views/list.py +++ b/explorer/views/list.py @@ -38,6 +38,7 @@ def get_context_data(self, **kwargs): context["object_list"] = self._build_queries_and_headers() context["recent_queries"] = self.recently_viewed() context["tasks_enabled"] = app_settings.ENABLE_TASKS + context["vite_dev_mode"] = app_settings.VITE_DEV_MODE return context def get_queryset(self): diff --git a/pyproject.toml b/ruff.toml similarity index 80% rename from pyproject.toml rename to ruff.toml index 381e2855..6ba6e47b 100644 --- a/pyproject.toml +++ b/ruff.toml @@ -1,6 +1,13 @@ -[tool.ruff] -# https://beta.ruff.rs/docs/configuration/ line-length = 120 + +extend-exclude = [ + ".ruff_cache", + ".env", + ".venv", + "**migrations/**", +] + +[lint] select = [ "E", # pycodestyle errors "W", # pycodestyle warnings @@ -15,18 +22,11 @@ select = [ "UP", # pyupgrade ] -extend-exclude = [ - ".ruff_cache", - ".env", - ".venv", - "**migrations/**", -] - ignore = [ "I001", # Import block is un-sorted or un-formatted (would be nice not to do this) ] -[tool.ruff.per-file-ignores] +[lint.per-file-ignores] "__init__.py" = [ "F401" # unused-import ] @@ -50,23 +50,12 @@ ignore = [ "PLR0913", # Too many arguments in function definition (8 > 5) ] -[tool.ruff.isort] +[lint.isort] combine-as-imports = true known-first-party = [ "explorer", ] -[tool.ruff.pyupgrade] +[lint.pyupgrade] # Preserve types, even if a file imports `from __future__ import annotations`. keep-runtime-typing = true - -[tool.coverage.run] -branch = true -parallel = true -source = ["django_nh3", "tests"] - -[tool.coverage.paths] -source = ["src", ".tox/py*/**/site-packages"] - -[tool.coverage.report] -show_missing = true diff --git a/test_project/start.sh b/test_project/start.sh index acd96f6c..1ac3deb2 100644 --- a/test_project/start.sh +++ b/test_project/start.sh @@ -1,18 +1,55 @@ -pip install -r requirements/base.txt -pip install -r requirements/optional.txt -python manage.py migrate -python manage.py shell </dev/null 2>&1; then + nvm install + nvm use +else + # Check Node version if nvm is not installed + current_node_version=$(node -v | cut -d. -f1 | sed 's/v//') + required_node_version=18 + if [ "$current_node_version" -lt "$required_node_version" ]; then + echo "Node version is less than 18.0 and nvm is not installed. Please install nvm or upgrade node and re-run." + proceed_with_npm=false + fi +fi + +if [ "$proceed_with_npm" = true ] ; then + npm install + # Start Django server in the background and get its PID + python ../manage.py runserver 0:8000 & + DJANGO_PID=$! + + # Set a trap to kill the Django server when the script exits + trap "echo 'Stopping Django server'; kill $DJANGO_PID" EXIT INT + + # Start Vite dev server + npm run dev +fi