diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 667846cdc..050d5b11d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,6 +1,11 @@ name: Test -on: [push, pull_request] +on: + push: + pull_request: + schedule: + # Run weekly on Saturday + - cron: '37 3 * * SAT' jobs: mysql: @@ -9,11 +14,11 @@ jobs: fail-fast: false max-parallel: 5 matrix: - python-version: ['3.6', '3.7', '3.8', '3.9'] + python-version: ['3.6', '3.7', '3.8', '3.9', '3.10'] services: mariadb: - image: mariadb:10.3 + image: mariadb env: MYSQL_ROOT_PASSWORD: debug_toolbar options: >- @@ -76,11 +81,11 @@ jobs: fail-fast: false max-parallel: 5 matrix: - python-version: ['3.6', '3.7', '3.8', '3.9'] + python-version: ['3.6', '3.7', '3.8', '3.9', '3.10'] services: postgres: - image: 'postgres:9.5' + image: postgres env: POSTGRES_DB: debug_toolbar POSTGRES_USER: debug_toolbar @@ -143,7 +148,7 @@ jobs: fail-fast: false max-parallel: 5 matrix: - python-version: ['3.6', '3.7', '3.8', '3.9'] + python-version: ['3.6', '3.7', '3.8', '3.9', '3.10'] steps: - uses: actions/checkout@v2 @@ -216,4 +221,4 @@ jobs: python -m pip install --upgrade tox - name: Test with tox - run: tox -e docs,style,packaging + run: tox -e docs,packaging diff --git a/.gitignore b/.gitignore index df5a2d10c..6caa61357 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,5 @@ docs/_build example/db.sqlite3 htmlcov .tox -node_modules -package-lock.json geckodriver.log coverage.xml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..30f6b7bb0 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,48 @@ +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.0.1 + hooks: + - id: check-yaml + - id: end-of-file-fixer + - id: trailing-whitespace + - id: mixed-line-ending +- repo: https://github.com/pycqa/flake8 + rev: 4.0.1 + hooks: + - id: flake8 +- repo: https://github.com/pycqa/doc8 + rev: 0.10.1 + hooks: + - id: doc8 +- repo: https://github.com/pycqa/isort + rev: 5.10.1 + hooks: + - id: isort +- repo: https://github.com/pre-commit/pygrep-hooks + rev: v1.9.0 + hooks: + - id: python-check-blanket-noqa + - id: python-check-mock-methods + - id: python-no-eval + - id: python-no-log-warn + - id: rst-backticks + - id: rst-directive-colons +- repo: https://github.com/pre-commit/mirrors-prettier + rev: v2.5.1 + hooks: + - id: prettier + types_or: [javascript, css] +- repo: https://github.com/pre-commit/mirrors-eslint + rev: v8.4.0 + hooks: + - id: eslint + files: \.js?$ + types: [file] + args: + - --fix +- repo: https://github.com/psf/black + rev: 21.12b0 + hooks: + - id: black + language_version: python3 + entry: black --target-version=py36 diff --git a/.tx/config b/.tx/config index bdbb9bf43..5c9ecc129 100644 --- a/.tx/config +++ b/.tx/config @@ -6,4 +6,3 @@ lang_map = sr@latin:sr_Latn file_filter = debug_toolbar/locale//LC_MESSAGES/django.po source_file = debug_toolbar/locale/en/LC_MESSAGES/django.po source_lang = en - diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..e0d5efab5 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,46 @@ +# Code of Conduct + +As contributors and maintainers of the Jazzband projects, and in the interest of +fostering an open and welcoming community, we pledge to respect all people who +contribute through reporting issues, posting feature requests, updating documentation, +submitting pull requests or patches, and other activities. + +We are committed to making participation in the Jazzband a harassment-free experience +for everyone, regardless of the level of experience, gender, gender identity and +expression, sexual orientation, disability, personal appearance, body size, race, +ethnicity, age, religion, or nationality. + +Examples of unacceptable behavior by participants include: + +- The use of sexualized language or imagery +- Personal attacks +- Trolling or insulting/derogatory comments +- Public or private harassment +- Publishing other's private information, such as physical or electronic addresses, + without explicit permission +- Other unethical or unprofessional conduct + +The Jazzband roadies have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are not +aligned to this Code of Conduct, or to ban temporarily or permanently any contributor +for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +By adopting this Code of Conduct, the roadies commit themselves to fairly and +consistently applying these principles to every aspect of managing the jazzband +projects. Roadies who do not follow or enforce the Code of Conduct may be permanently +removed from the Jazzband roadies. + +This code of conduct applies both within project spaces and in public spaces when an +individual is representing the project or its community. + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by +contacting the roadies at `roadies@jazzband.co`. All complaints will be reviewed and +investigated and will result in a response that is deemed necessary and appropriate to +the circumstances. Roadies are obligated to maintain confidentiality with regard to the +reporter of an incident. + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version +1.3.0, available at [https://contributor-covenant.org/version/1/3/0/][version] + +[homepage]: https://contributor-covenant.org +[version]: https://contributor-covenant.org/version/1/3/0/ diff --git a/LICENSE b/LICENSE index 15d830926..221d73313 100644 --- a/LICENSE +++ b/LICENSE @@ -4,10 +4,10 @@ All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - 1. Redistributions of source code must retain the above copyright notice, + 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - - 2. Redistributions in binary form must reproduce the above copyright + + 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. diff --git a/Makefile b/Makefile index 5b5ca4d76..1600496e5 100644 --- a/Makefile +++ b/Makefile @@ -1,22 +1,4 @@ -.PHONY: flake8 example test coverage translatable_strings update_translations - -PRETTIER_TARGETS = '**/*.(css|js)' - -style: package-lock.json - isort . - black --target-version=py36 . - flake8 - npx eslint --ignore-path .gitignore --fix . - npx prettier --ignore-path .gitignore --write $(PRETTIER_TARGETS) - ! grep -r '\(style=\|onclick=\|" # noqa + local_var = "" # noqa: F841 list(User.objects.filter(username="café".encode("utf-8"))) response = self.panel.process_request(self.request) self.panel.generate_stats(self.request, response) diff --git a/tests/panels/test_staticfiles.py b/tests/panels/test_staticfiles.py index d660b3c77..32ed7ea61 100644 --- a/tests/panels/test_staticfiles.py +++ b/tests/panels/test_staticfiles.py @@ -26,9 +26,10 @@ def test_default_case(self): ) self.assertEqual(self.panel.num_used, 0) self.assertNotEqual(self.panel.num_found, 0) - self.assertEqual( - self.panel.get_staticfiles_apps(), ["django.contrib.admin", "debug_toolbar"] - ) + expected_apps = ["django.contrib.admin", "debug_toolbar"] + if settings.USE_GIS: + expected_apps = ["django.contrib.gis"] + expected_apps + self.assertEqual(self.panel.get_staticfiles_apps(), expected_apps) self.assertEqual( self.panel.get_staticfiles_dirs(), finders.FileSystemFinder().locations ) @@ -74,9 +75,10 @@ def test_finder_directory_does_not_exist(self): ) self.assertEqual(self.panel.num_used, 0) self.assertNotEqual(self.panel.num_found, 0) - self.assertEqual( - self.panel.get_staticfiles_apps(), ["django.contrib.admin", "debug_toolbar"] - ) + expected_apps = ["django.contrib.admin", "debug_toolbar"] + if settings.USE_GIS: + expected_apps = ["django.contrib.gis"] + expected_apps + self.assertEqual(self.panel.get_staticfiles_apps(), expected_apps) self.assertEqual( self.panel.get_staticfiles_dirs(), finders.FileSystemFinder().locations ) diff --git a/tests/settings.py b/tests/settings.py index b7ca35faf..63456a2f6 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -27,9 +27,15 @@ "tests", ] +USE_GIS = os.getenv("DB_BACKEND") in ("postgis",) + +if USE_GIS: + INSTALLED_APPS = ["django.contrib.gis"] + INSTALLED_APPS + MEDIA_URL = "/media/" # Avoids https://code.djangoproject.com/ticket/21451 MIDDLEWARE = [ + "tests.middleware.UseCacheAfterToolbar", "debug_toolbar.middleware.DebugToolbarMiddleware", "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", @@ -81,7 +87,9 @@ DATABASES = { "default": { - "ENGINE": "django.db.backends.%s" % os.getenv("DB_BACKEND", "sqlite3"), + "ENGINE": "django.{}db.backends.{}".format( + "contrib.gis." if USE_GIS else "", os.getenv("DB_BACKEND", "sqlite3") + ), "NAME": os.getenv("DB_NAME", ":memory:"), "USER": os.getenv("DB_USER"), "PASSWORD": os.getenv("DB_PASSWORD"), diff --git a/tests/test_checks.py b/tests/test_checks.py index a1c59614a..15464f9a2 100644 --- a/tests/test_checks.py +++ b/tests/test_checks.py @@ -122,3 +122,37 @@ def test_panels_is_empty(self): ) ], ) + + @override_settings( + TEMPLATES=[ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "APP_DIRS": False, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ] + }, + }, + ] + ) + def test_templates_is_using_app_dirs_false(self): + errors = run_checks() + self.assertEqual( + errors, + [ + Warning( + "At least one DjangoTemplates TEMPLATES configuration " + "needs to have APP_DIRS set to True.", + hint=( + "Use APP_DIRS=True for at least one " + "django.template.backends.django.DjangoTemplates " + "backend configuration." + ), + id="debug_toolbar.W006", + ) + ], + ) diff --git a/tests/test_integration.py b/tests/test_integration.py index 6d3208fff..006ab93ee 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -1,11 +1,14 @@ +import json import os import re import unittest import django import html5lib +from django.conf import settings from django.contrib.staticfiles.testing import StaticLiveServerTestCase from django.core import signing +from django.core.cache import cache from django.db import connection from django.http import HttpResponse from django.template.loader import get_template @@ -15,6 +18,7 @@ from debug_toolbar.forms import SignedDataForm from debug_toolbar.middleware import DebugToolbarMiddleware, show_toolbar from debug_toolbar.panels import Panel +from debug_toolbar.panels.sql.forms import SQLSelectForm from debug_toolbar.toolbar import DebugToolbar from .base import BaseTestCase, IntegrationTestCase @@ -97,11 +101,44 @@ def get_response(request): self.assertContains(response, "\n") def test_cache_page(self): + # Clear the cache before testing the views. Other tests that use cached_view + # may run earlier and cause fewer cache calls. + cache.clear() self.client.get("/cached_view/") self.assertEqual(len(self.toolbar.get_panel_by_id("CachePanel").calls), 3) self.client.get("/cached_view/") self.assertEqual(len(self.toolbar.get_panel_by_id("CachePanel").calls), 5) + @override_settings(ROOT_URLCONF="tests.urls_use_package_urls") + def test_include_package_urls(self): + """Test urlsconf that uses the debug_toolbar.urls in the include call""" + # Clear the cache before testing the views. Other tests that use cached_view + # may run earlier and cause fewer cache calls. + cache.clear() + self.client.get("/cached_view/") + self.assertEqual(len(self.toolbar.get_panel_by_id("CachePanel").calls), 3) + self.client.get("/cached_view/") + self.assertEqual(len(self.toolbar.get_panel_by_id("CachePanel").calls), 5) + + def test_low_level_cache_view(self): + """Test cases when low level caching API is used within a request.""" + self.client.get("/cached_low_level_view/") + self.assertEqual(len(self.toolbar.get_panel_by_id("CachePanel").calls), 2) + self.client.get("/cached_low_level_view/") + self.assertEqual(len(self.toolbar.get_panel_by_id("CachePanel").calls), 3) + + def test_cache_disable_instrumentation(self): + """ + Verify that middleware cache usages before and after + DebugToolbarMiddleware are not counted. + """ + self.assertIsNone(cache.set("UseCacheAfterToolbar.before", None)) + self.assertIsNone(cache.set("UseCacheAfterToolbar.after", None)) + self.client.get("/execute_sql/") + self.assertEqual(cache.get("UseCacheAfterToolbar.before"), 1) + self.assertEqual(cache.get("UseCacheAfterToolbar.after"), 1) + self.assertEqual(len(self.toolbar.get_panel_by_id("CachePanel").calls), 0) + def test_is_toolbar_request(self): self.request.path = "/__debug__/render_panel/" self.assertTrue(self.toolbar.is_toolbar_request(self.request)) @@ -273,6 +310,28 @@ def test_sql_explain_checks_show_toolbar(self): ) self.assertEqual(response.status_code, 404) + @unittest.skipUnless(settings.USE_GIS, "Test only valid with gis support") + def test_sql_explain_gis(self): + from django.contrib.gis.geos import GEOSGeometry + + from .models import Location + + db_table = Location._meta.db_table + + url = "/__debug__/sql_explain/" + geom = GEOSGeometry("POLYGON((0 0, 0 1, 1 1, 0 0))") + data = { + "sql": f'SELECT "{db_table}"."point" FROM "{db_table}" WHERE "{db_table}"."point" @ {geom.hex} LIMIT 1', + "raw_sql": f'SELECT "{db_table}"."point" FROM "{db_table}" WHERE "{db_table}"."point" @ %s LIMIT 1', + "params": json.dumps([geom.hex]), + "alias": "default", + "duration": "0", + } + data["hash"] = SQLSelectForm().make_hash(data) + + response = self.client.post(url, data=data) + self.assertEqual(response.status_code, 200) + @unittest.skipUnless( connection.vendor == "postgresql", "Test valid only on PostgreSQL" ) @@ -376,7 +435,7 @@ def test_view_returns_template_response(self): self.assertEqual(response.status_code, 200) @override_settings(DEBUG_TOOLBAR_CONFIG={"DISABLE_PANELS": set()}) - def test_incercept_redirects(self): + def test_intcercept_redirects(self): response = self.client.get("/redirect/") self.assertEqual(response.status_code, 200) # Link to LOCATION header. diff --git a/tests/urls.py b/tests/urls.py index cef00e3e2..c12fc744a 100644 --- a/tests/urls.py +++ b/tests/urls.py @@ -2,8 +2,6 @@ from django.contrib.auth.views import LoginView from django.urls import include, path, re_path -import debug_toolbar - from . import views from .models import NonAsciiRepr @@ -20,9 +18,10 @@ path("new_user/", views.new_user), path("execute_sql/", views.execute_sql), path("cached_view/", views.cached_view), + path("cached_low_level_view/", views.cached_low_level_view), path("json_view/", views.json_view), path("redirect/", views.redirect_view), path("login_without_redirect/", LoginView.as_view(redirect_field_name=None)), path("admin/", admin.site.urls), - path("__debug__/", include(debug_toolbar.urls)), + path("__debug__/", include("debug_toolbar.urls")), ] diff --git a/tests/urls_use_package_urls.py b/tests/urls_use_package_urls.py new file mode 100644 index 000000000..50f7dfd69 --- /dev/null +++ b/tests/urls_use_package_urls.py @@ -0,0 +1,11 @@ +"""urls.py to test using debug_toolbar.urls in include""" +from django.urls import include, path + +import debug_toolbar + +from . import views + +urlpatterns = [ + path("cached_view/", views.cached_view), + path("__debug__/", include(debug_toolbar.urls)), +] diff --git a/tests/views.py b/tests/views.py index 15c0c18ec..b2fd21c54 100644 --- a/tests/views.py +++ b/tests/views.py @@ -1,4 +1,5 @@ from django.contrib.auth.models import User +from django.core.cache import cache from django.http import HttpResponseRedirect, JsonResponse from django.shortcuts import render from django.template.response import TemplateResponse @@ -33,6 +34,15 @@ def cached_view(request): return render(request, "base.html") +def cached_low_level_view(request): + key = "spam" + value = cache.get(key) + if not value: + value = "eggs" + cache.set(key, value, 60) + return render(request, "base.html") + + def json_view(request): return JsonResponse({"foo": "bar"}) diff --git a/tox.ini b/tox.ini index c3bb0bac2..3abd404bc 100644 --- a/tox.ini +++ b/tox.ini @@ -1,19 +1,20 @@ [tox] envlist = docs - style packaging - py{36,37}-dj{22,31,32}-sqlite - py{38,39}-dj{22,31,32,main}-sqlite - py{36,37,38,39}-dj{22,31,32}-{postgresql,mysql} + py{36,37}-dj{22,31,32}-{sqlite,postgresql,postgis,mysql} + py{38,39}-dj{22,31,32,40,main}-{sqlite,postgresql,postgis,mysql} + py{310}-dj{32,40,main}-{sqlite,postgresql,postgis,mysql} [testenv] deps = dj22: Django==2.2.* dj31: Django==3.1.* - dj32: Django>=3.2a1,<4.0 + dj32: Django>=3.2,<4.0 + dj40: Django>=4.0rc1,<4.1 sqlite: mock postgresql: psycopg2-binary + postgis: psycopg2-binary mysql: mysqlclient djmain: https://github.com/django/django/archive/main.tar.gz coverage @@ -33,7 +34,7 @@ passenv= setenv = PYTHONPATH = {toxinidir} PYTHONWARNINGS = d - py38-dj31-postgresql: DJANGO_SELENIUM_TESTS = true + py39-dj32-postgresql: DJANGO_SELENIUM_TESTS = true DB_NAME = {env:DB_NAME:debug_toolbar} DB_USER = {env:DB_USER:debug_toolbar} DB_HOST = {env:DB_HOST:localhost} @@ -42,19 +43,19 @@ whitelist_externals = make pip_pre = True commands = make coverage TEST_ARGS='{posargs:tests}' -[testenv:py{36,37,38,39}-dj{22,31,32}-postgresql] +[testenv:py{36,37,38,39,310}-dj{22,31,32}-postgresql] setenv = {[testenv]setenv} DB_BACKEND = postgresql DB_PORT = {env:DB_PORT:5432} -[testenv:py{36,37,38,39}-dj{22,31,32}-mysql] +[testenv:py{36,37,38,39,310}-dj{22,31,32}-mysql] setenv = {[testenv]setenv} DB_BACKEND = mysql DB_PORT = {env:DB_PORT:3306} -[testenv:py{36,37,38,39}-dj{22,31,32,main}-sqlite] +[testenv:py{36,37,38,39,310}-dj{22,31,32,main}-sqlite] setenv = {[testenv]setenv} DB_BACKEND = sqlite3 @@ -66,14 +67,6 @@ deps = Sphinx sphinxcontrib-spelling -[testenv:style] -commands = make style_check -deps = - black>=19.10b0 - flake8 - isort>=5.0.2 -skip_install = true - [testenv:packaging] commands = python setup.py sdist bdist_wheel @@ -90,9 +83,11 @@ python = 3.7: py37 3.8: py38 3.9: py39 + 3.10: py310 [gh-actions:env] DB_BACKEND = mysql: mysql postgresql: postgresql + postgis: postgresql sqlite3: sqlite