Repository: django-silk/silk Branch: master Commit: dd9328662f88 Files: 221 Total size: 2.2 MB Directory structure: gitextract_azh7ec58/ ├── .coveragerc ├── .github/ │ └── workflows/ │ ├── release.yml │ └── test.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── docs/ │ ├── Makefile │ ├── conf.py │ ├── configuration.rst │ ├── index.rst │ ├── profiling.rst │ ├── quickstart.rst │ └── troubleshooting.rst ├── gulpfile.js ├── package.json ├── project/ │ ├── example_app/ │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── migrations/ │ │ │ ├── 0001_initial.py │ │ │ ├── 0002_alter_blind_photo.py │ │ │ ├── 0003_blind_unique_name_if_provided.py │ │ │ └── __init__.py │ │ ├── models.py │ │ ├── templates/ │ │ │ └── example_app/ │ │ │ ├── blind_form.html │ │ │ ├── index.html │ │ │ └── login.html │ │ ├── tests.py │ │ ├── urls.py │ │ └── views.py │ ├── manage.py │ ├── project/ │ │ ├── __init__.py │ │ ├── settings.py │ │ └── urls.py │ ├── tests/ │ │ ├── __init__.py │ │ ├── data/ │ │ │ ├── __init__.py │ │ │ └── dynamic.py │ │ ├── factories.py │ │ ├── test_app_config.py │ │ ├── test_code.py │ │ ├── test_code_gen_curl.py │ │ ├── test_code_gen_django.py │ │ ├── test_collector.py │ │ ├── test_command_garbage_collect.py │ │ ├── test_compat.py │ │ ├── test_config_auth.py │ │ ├── test_config_long_urls.py │ │ ├── test_config_max_body_size.py │ │ ├── test_config_meta.py │ │ ├── test_db.py │ │ ├── test_dynamic_profiling.py │ │ ├── test_encoding.py │ │ ├── test_end_points.py │ │ ├── test_execute_sql.py │ │ ├── test_filters.py │ │ ├── test_lib/ │ │ │ ├── __init__.py │ │ │ ├── assertion.py │ │ │ └── mock_suite.py │ │ ├── test_models.py │ │ ├── test_multipart_forms.py │ │ ├── test_profile_dot.py │ │ ├── test_profile_parser.py │ │ ├── test_response_assumptions.py │ │ ├── test_sensitive_data_in_request.py │ │ ├── test_silky_middleware.py │ │ ├── test_silky_profiler.py │ │ ├── test_view_clear_db.py │ │ ├── test_view_profiling.py │ │ ├── test_view_requests.py │ │ ├── test_view_sql_detail.py │ │ ├── test_view_summary_view.py │ │ ├── urlconf_without_silk.py │ │ └── util.py │ └── wsgi.py ├── pyproject.toml ├── pytest.ini ├── requirements.txt ├── scss/ │ ├── components/ │ │ ├── cell.scss │ │ ├── colors.scss │ │ ├── fonts.scss │ │ ├── heading.scss │ │ ├── numeric.scss │ │ ├── row.scss │ │ └── summary.scss │ └── pages/ │ ├── base.scss │ ├── clear_db.scss │ ├── cprofile.scss │ ├── detail_base.scss │ ├── profile_detail.scss │ ├── profiling.scss │ ├── raw.scss │ ├── request.scss │ ├── requests.scss │ ├── root_base.scss │ ├── sql.scss │ ├── sql_detail.scss │ └── summary.scss ├── setup.py ├── silk/ │ ├── __init__.py │ ├── apps.py │ ├── auth.py │ ├── code_generation/ │ │ ├── __init__.py │ │ ├── curl.py │ │ └── django_test_client.py │ ├── collector.py │ ├── config.py │ ├── errors.py │ ├── management/ │ │ ├── __init__.py │ │ └── commands/ │ │ ├── __init__.py │ │ ├── silk_clear_request_log.py │ │ └── silk_request_garbage_collect.py │ ├── middleware.py │ ├── migrations/ │ │ ├── 0001_initial.py │ │ ├── 0002_auto_update_uuid4_id_field.py │ │ ├── 0003_request_prof_file.py │ │ ├── 0004_request_prof_file_storage.py │ │ ├── 0005_increase_request_prof_file_length.py │ │ ├── 0006_fix_request_prof_file_blank.py │ │ ├── 0007_sqlquery_identifier.py │ │ ├── 0008_sqlquery_analysis.py │ │ └── __init__.py │ ├── model_factory.py │ ├── models.py │ ├── profiling/ │ │ ├── __init__.py │ │ ├── dynamic.py │ │ └── profiler.py │ ├── request_filters.py │ ├── singleton.py │ ├── sql.py │ ├── static/ │ │ └── silk/ │ │ ├── css/ │ │ │ ├── components/ │ │ │ │ ├── cell.css │ │ │ │ ├── colors.css │ │ │ │ ├── fonts.css │ │ │ │ ├── heading.css │ │ │ │ ├── numeric.css │ │ │ │ ├── row.css │ │ │ │ └── summary.css │ │ │ └── pages/ │ │ │ ├── base.css │ │ │ ├── clear_db.css │ │ │ ├── cprofile.css │ │ │ ├── detail_base.css │ │ │ ├── profile_detail.css │ │ │ ├── profiling.css │ │ │ ├── raw.css │ │ │ ├── request.css │ │ │ ├── requests.css │ │ │ ├── root_base.css │ │ │ ├── sql.css │ │ │ ├── sql_detail.css │ │ │ └── summary.css │ │ ├── js/ │ │ │ ├── components/ │ │ │ │ ├── cell.js │ │ │ │ └── filters.js │ │ │ └── pages/ │ │ │ ├── base.js │ │ │ ├── clear_db.js │ │ │ ├── detail_base.js │ │ │ ├── profile_detail.js │ │ │ ├── profiling.js │ │ │ ├── raw.js │ │ │ ├── request.js │ │ │ ├── requests.js │ │ │ ├── root_base.js │ │ │ ├── sql.js │ │ │ ├── sql_detail.js │ │ │ └── summary.js │ │ └── lib/ │ │ ├── highlight/ │ │ │ ├── foundation.css │ │ │ └── highlight.pack.js │ │ ├── jquery.datetimepicker.css │ │ ├── jquery.datetimepicker.js │ │ ├── sortable.js │ │ └── viz-lite.js │ ├── storage.py │ ├── templates/ │ │ └── silk/ │ │ ├── base/ │ │ │ ├── base.html │ │ │ ├── detail_base.html │ │ │ └── root_base.html │ │ ├── clear_db.html │ │ ├── cprofile.html │ │ ├── inclusion/ │ │ │ ├── code.html │ │ │ ├── heading.html │ │ │ ├── profile_menu.html │ │ │ ├── profile_summary.html │ │ │ ├── request_menu.html │ │ │ ├── request_summary.html │ │ │ ├── request_summary_row.html │ │ │ └── root_menu.html │ │ ├── profile_detail.html │ │ ├── profiling.html │ │ ├── raw.html │ │ ├── request.html │ │ ├── requests.html │ │ ├── sql.html │ │ ├── sql_detail.html │ │ └── summary.html │ ├── templatetags/ │ │ ├── __init__.py │ │ ├── silk_filters.py │ │ ├── silk_inclusion.py │ │ ├── silk_nav.py │ │ └── silk_urls.py │ ├── urls.py │ ├── utils/ │ │ ├── __init__.py │ │ ├── data_deletion.py │ │ ├── pagination.py │ │ └── profile_parser.py │ └── views/ │ ├── __init__.py │ ├── clear_db.py │ ├── code.py │ ├── cprofile.py │ ├── profile_detail.py │ ├── profile_dot.py │ ├── profile_download.py │ ├── profiling.py │ ├── raw.py │ ├── request_detail.py │ ├── requests.py │ ├── sql.py │ ├── sql_detail.py │ └── summary.py ├── silk.sublime-project ├── tox.ini └── web.psd ================================================ FILE CONTENTS ================================================ ================================================ FILE: .coveragerc ================================================ [run] branch = True source = silk ================================================ FILE: .github/workflows/release.yml ================================================ name: Release on: push: tags: - '*' jobs: build: if: github.repository == 'jazzband/django-silk' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up Python uses: actions/setup-python@v5 with: python-version: 3.14 - name: Install dependencies run: | python -m pip install -U pip python -m pip install -U setuptools twine wheel - name: Build package run: | python setup.py --version python setup.py sdist --format=gztar bdist_wheel twine check dist/* - name: Upload packages to Jazzband if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') uses: pypa/gh-action-pypi-publish@master with: user: jazzband password: ${{ secrets.JAZZBAND_RELEASE_KEY }} repository_url: https://jazzband.co/projects/django-silk/upload ================================================ FILE: .github/workflows/test.yml ================================================ name: Test on: [push, pull_request] jobs: build: name: build (Python ${{ matrix.python-version }}, Django ${{ matrix.django-version }}) runs-on: ubuntu-latest strategy: fail-fast: false matrix: python-version: ['3.10', '3.11', '3.12', '3.13', '3.14'] django-version: ['4.2', '5.1', '5.2', '6.0', 'main'] postgres-version: ['14', '18'] mariadb-version: ['10.6', '10.11', '11.4', '11.8'] exclude: # Django 4.2 doesn't support Python >= 3.13 - django-version: '4.2' python-version: '3.13' - django-version: '4.2' python-version: '3.14' # Django 5.1 doesn't support Python >= 3.14 - django-version: '5.1' python-version: '3.14' # Django 6.0 doesn't support Python <3.12 (https://docs.djangoproject.com/en/dev/releases/6.0/#python-compatibility) - django-version: '6.0' python-version: '3.10' - django-version: '6.0' python-version: '3.11' - django-version: 'main' python-version: '3.10' - django-version: 'main' python-version: '3.11' services: postgres: image: postgres:${{ matrix.postgres-version }} env: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres POSTGRES_DB: postgres ports: - 5432:5432 options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 mariadb: image: mariadb:${{ matrix.mariadb-version }} env: MYSQL_ROOT_PASSWORD: mysql MYSQL_DATABASE: mysql options: >- --health-cmd "mariadb-admin ping" --health-interval 10s --health-timeout 5s --health-retries 5 ports: - 3306:3306 steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Get pip cache dir id: pip-cache run: | echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT - name: Cache uses: actions/cache@v4 with: path: ${{ steps.pip-cache.outputs.dir }} key: ${{ matrix.python-version }}-v1-${{ hashFiles('**/requirements.txt') }}-${{ hashFiles('**/setup.py') }}-${{ hashFiles('**/tox.ini') }} restore-keys: | ${{ matrix.python-version }}-v1- - name: Install dependencies run: | python -m pip install --upgrade pip python -m pip install --upgrade tox tox-gh-actions - name: Tox tests run: | tox -v env: DJANGO: ${{ matrix.django-version }} - name: Upload coverage uses: codecov/codecov-action@v3 with: name: Python ${{ matrix.python-version }} ================================================ FILE: .gitignore ================================================ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] # C extensions *.so # Distribution / packaging .Python env/ bin/ build/ develop-eggs/ dist/ eggs/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg .eggs # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .pytest_cache/ .coverage .cache nosetests.xml coverage.xml # Translations *.mo # Mr Developer .mr.developer.cfg .project .pydevproject # Rope .ropeproject # Django stuff: *.log *.pot # Sphinx documentation docs/_build/ # Other dist .idea *db.sqlite* /django_silky/media *.prof project/media/ project/tmp/ .vscode/ # Hardlinks /django_silky/silk # Pip /src # Sphinx _html # Tox .tox.ini.swp # Node node_modules # Gulp .gulp-scss-cache .sass-cache *~ .DS_Store ### PyCharm ### # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm *.iml ## Directory-based project format: .idea/ ## File-based project format: *.ipr *.iws ## Plugin-specific files: # IntelliJ /out/ # mpeltonen/sbt-idea plugin .idea_modules/ # JIRA plugin atlassian-ide-plugin.xml # Crashlytics plugin (for Android Studio and IntelliJ) com_crashlytics_export_strings.xml crashlytics.properties crashlytics-build.properties fabric.properties # Virtual env .venv* package-lock.json *.db ================================================ FILE: .pre-commit-config.yaml ================================================ repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: 'v6.0.0' hooks: - id: check-merge-conflict - repo: https://github.com/hadialqattan/pycln rev: v2.6.0 hooks: - id: pycln args: ['--all'] - repo: https://github.com/asottile/yesqa rev: v1.5.0 hooks: - id: yesqa - repo: https://github.com/pycqa/isort rev: '8.0.1' hooks: - id: isort args: ['--profile', 'black'] - repo: https://github.com/pre-commit/pre-commit-hooks rev: 'v6.0.0' hooks: - id: end-of-file-fixer exclude: >- ^docs/[^/]*\.svg$ - id: requirements-txt-fixer - id: trailing-whitespace types: [python] - id: file-contents-sorter files: | CONTRIBUTORS.txt| docs/spelling_wordlist.txt| .gitignore| .gitattributes - id: check-case-conflict - id: check-json - id: check-xml - id: check-toml - id: check-xml - id: check-yaml - id: debug-statements - id: check-added-large-files - id: check-symlinks - id: debug-statements - id: detect-aws-credentials args: ['--allow-missing-credentials'] - id: detect-private-key exclude: ^examples|(?:tests/ssl)/ - repo: https://github.com/asottile/pyupgrade rev: 'v3.21.2' hooks: - id: pyupgrade args: ['--keep-mock'] - repo: https://github.com/adamchainz/django-upgrade rev: '1.30.0' hooks: - id: django-upgrade args: [--target-version, '4.2'] - repo: https://github.com/hhatto/autopep8 rev: 'v2.3.2' hooks: - id: autopep8 - repo: https://github.com/PyCQA/flake8 rev: '7.3.0' hooks: - id: flake8 exclude: '^docs/' - repo: https://github.com/Lucas-C/pre-commit-hooks-markup rev: v1.0.1 hooks: - id: rst-linter files: >- ^[^/]+[.]rst$ ================================================ FILE: CHANGELOG.md ================================================ # Change Log ## Unreleased ## [5.5.0](https://github.com/jazzband/django-silk/tree/5.5.0) (2026-03-06) :release-by: Albert Wang (@albertyw) [Full Changelog](https://github.com/jazzband/django-silk/compare/5.4.3..5.5.0) **Fixes:** - Fix context manager for `_process_response` (#827) @izabala033 - Fix mouse event for sql navigation (#847) @albertyw **Features/Enhancements:** - Add support for Django 6.0 (#836) @albertyw - Add support for Python 3.14 (#834) @albertyw - Get paginator limit from URL params (#646) @strig - Hide pagination when there's only one page (#844) @ShlomoCode **Maintenance and Cleanup:** - Remove official support for Python 3.9 (#834) @albertyw - Dependency updates ## [5.4.3](https://github.com/jazzband/django-silk/tree/5.4.3) (2025-09-08) :release-by: Albert Wang (@albertyw) [Full Changelog](https://github.com/jazzband/django-silk/compare/5.4.2..5.4.3) **Fixes:** - Fix double EXPLAIN when calling explain on queryset (#654) @stereodamage - Fix serialization issues for binary and json fields (#821) @albertyw ## [5.4.2](https://github.com/jazzband/django-silk/tree/5.4.2) (2025-08-17) :release-by: Albert Wang (@albertyw) [Full Changelog](https://github.com/jazzband/django-silk/compare/5.4.1..5.4.2) **Fixes:** - Reverts #798 which causes issues when serializing JSONFields (#807) @albertyw - Also reverts #798 which has a race condition when modifying `execute_sql` (#816) @albertyw - Catch and ignore sql encoding errors (#810) @albertyw @bpascard **Maintenance and Cleanup:** - Document that context_processors.request is required (#815) @albertyw - Fix documentation formatting (#810) @albertyw - Test refactors (#814) @albertyw ## [5.4.1](https://github.com/jazzband/django-silk/tree/5.4.1) (2025-08-10) :release-by: Albert Wang (@albertyw) [Full Changelog](https://github.com/jazzband/django-silk/compare/5.4.0..5.4.1) **Fixes:** - Fixes curl/client values rendering in request_detail (#797) @bcmyguest - Fix serialization of non-unicode binary data, add cleanup in middleware (#798) @glennmatthews - Make transactions target the DB alias selected by the router (#801) @OscarVanL **Maintenance and Cleanup:** - Dependency updates - Documentation updates ## [5.4.0](https://github.com/jazzband/django-silk/tree/5.4.0) (2025-05-03) :release-by: Albert Wang (@albertyw) [Full Changelog](https://github.com/jazzband/django-silk/compare/5.3.2..5.4.0) **Note: this release removes support for Django 5.0** **Note: this release removes autoformatting of python snippets; continue formatting by pip installing `django-silk[formatting]`** **Features/Enhancements:** - Add support for Django 5.2 (#784) @albertyw - Support opening SQL details in a new window (#788) @joaopedroalbq - Avoid timeouts when deserializing large jsons (#768) @quertenmont - Make autopep8 optional (#782) @albertyw **Fixes:** - Fix masking sensitive data when an empty `SILKY_SENSITIVE_KEYS` is provided (#777) @ahsanshafiq742 **Maintenance and Cleanup:** - Remove support for Django 5.0 (#783) @albertyw - Fix logger deprecations (#766) @rjdebastiani - Update dependencies and various autoupdate cleanups ## [5.3.2](https://github.com/jazzband/django-silk/tree/5.3.2) (2024-12-05) :release-by: Albert Wang (@albertyw) [Full Changelog](https://github.com/jazzband/django-silk/compare/5.3.1..5.3.2) **Fixes:** - Fix missing image from jQuery UI 1.13.2 (#757) @Tatsh **Maintenance and Cleanup:** - Adds updated documentation on middleware ordering (#758) @SoyJoseC - Updated python dependencies (#761, #760) @albertyw ## [5.3.1](https://github.com/jazzband/django-silk/tree/5.3.1) (2024-11-08) :release-by: Albert Wang (@albertyw) [Full Changelog](https://github.com/jazzband/django-silk/compare/5.3.0..5.3.1) **Fixes:** - Fix missing jQuery UI images (#754) @Tatsh - Fix swallowing exceptions when processing response in silk middleware (#753) @albertyw ## [5.3.0](https://github.com/jazzband/django-silk/tree/5.3.0) (2024-10-25) :release-by: Albert Wang (@albertyw) [Full Changelog](https://github.com/jazzband/django-silk/compare/5.2.0..5.3.0) **Note: this release removes support for Django 3.2 and Python 3.8** **Features/Enhancements:** - Support python 3.13 (#747) **Fixes:** - Upgrade jQuery-UI to 1.13.2 to fix XSS vulnerability (#742) **Maintenance and Cleanup:** - Remove Django 3.2 support (#736) - Drop support for python 3.8 (#749) - Update python dependencies (#748) ## [5.2.0](https://github.com/jazzband/django-silk/tree/5.2.0) (2024-08-17) :release-by: Albert Wang (@albertyw) [Full Changelog](https://github.com/jazzband/django-silk/compare/5.1.0..5.2.0) **Features/Enhancements:** - Support Django 5.1 (#734, #732) @albertyw **Fixes:** - Fix when Session, Authentication or Message middleware are not present (#667) @mgaligniana - Update 'tables_involved' property to include tables from UPDATE operation (#717) @emregeldegul - Fix double-escaping of the curl and Python example code (#709) @SpecLad - Correct units in profiling and requests pages (#725) @ka28kumar **Maintenance and Cleanup:** - Update python dependencies (#733) @albertyw - Refactor SQL query time calculation to use Django aggregation (#720) @beltagymohamed - Fix test failures on Windows (#707) @SpecLad - Update workflow actions (#700) @albertyw - Update test matrix to latest version of django, postgres, and mariadb #701) @albertyw ## [5.1.0](https://github.com/jazzband/django-silk/tree/5.1.0) (2023-12-30) :release-by: Albert Wang (@albertyw) [Full Changelog](https://github.com/jazzband/django-silk/compare/5.0.4..5.1.0) **Upgrading:** This release includes [Fix deprecation warning for get_storage_class #669](https://github.com/jazzband/django-silk/pull/669) which deprecates `SILKY_STORAGE_CLASS`. Users should instead use the Django `STORAGES` configuration. See [README](https://github.com/albertyw/django-silk/blob/master/README.md#profiling) and [Django documentation](https://docs.djangoproject.com/en/5.0/ref/settings/#std-setting-STORAGES) for more information. Also, for python 3.12, the `cProfile` stdlib library cannot be enabled multiple times concurrently. Silk will therefore skip profiling if another profile is already enabled. **Features/Enhancements:** - Allow option to delete profiles (#652) @viralj **Fixes:** - Gracefully error out when there are concurrent profilers (#692) @albertyw - Always disable cProfile as part of cleanup (#699) @albertyw - Fix when Session, Authentication or Message middlewares are not present (#667) @mgaligniana **Maintenance and Cleanup:** - Fix deprecation warning for get_storage_class (#669) @albertyw - Support Django 4.2 (#685) @albertyw - Support python 3.12 (#683) @albertyw - Support Django 5 (#686) @albertyw - Remove deprecated datetime.timezone.utc (#687) @albertyw - Derive version from importlib (#697) @robinchow **Dependencies:** - Update python dependencies (#693) @albertyw ## [5.0.4](https://github.com/jazzband/django-silk/tree/5.0.4) (2023-09-17) :release-by: Albert Wang (@albertyw) [Full Changelog](https://github.com/jazzband/django-silk/compare/5.0.3..5.0.4) **Features/Enhancements:** - Handle case-insensitive sensitive headers (#674) @shtimn - Add a "pagetitle" block to Silky templates (#661) @vsajip - Allow to generate more informative profile file name (#638) @k4rl85 **Maintenance and Cleanup:** - Remove unsupported versions of Django and Python (#668) @albertyw - Outsource all inline scripts and styles (#635) @sgelis - Remove support for looking up headers on django <3.2 (#643) @albertyw **Dependencies:** - Update python dependencies (#677) @albertyw ## [5.0.3](https://github.com/jazzband/django-silk/tree/5.0.3) (2023-01-12) :release-by: Albert Wang (@albertyw) [Full Changelog](https://github.com/jazzband/django-silk/compare/5.0.2..5.0.3) **Fixes:** - #46 Retain ordering, view style and limit (#614) - #157 prevent encoding errors in params (#617) - #594 Silk fails on constraint check queries (#618) (Fixes compatibility with Django 4.1) **Features/Enhancements:** - #132 Add action on sql query list (#611) - traceback only when needed (#387) **Dependencies:** - #625 Drop dependency to jinja2 ## [5.0.2](https://github.com/jazzband/django-silk/tree/5.0.2) (2022-10-12) :release-by: Albert Wang (@albertyw) [Full Changelog](https://github.com/jazzband/django-silk/compare/5.0.1...5.0.2) **Fixes:** - Multipart forms and RawPostDataException (#592) - Decrease unnecessary database hits (#587) (#588) **Features/Enhancements:** - Remove unneeded pytz package (#603) - Use contextlib in test_profile_parser (#590) - Add support for storages, that don't implement full filesystem path (#596) ## [5.0.1](https://github.com/jazzband/django-silk/tree/5.0.1) (2022-07-03) :release-by: Albert Wang (@albertyw) [Full Changelog](https://github.com/jazzband/django-silk/compare/5.0.0...5.0.1) **Fixes:** - Add jquery UI 1.13.1 images and fix collectstatic (#576) ## [5.0.0](https://github.com/jazzband/django-silk/tree/5.0.0) (2022-06-20) :release-by: Albert Wang (@albertyw) [Full Changelog](https://github.com/jazzband/django-silk/compare/4.4.0...5.0.0) **Features/Enhancements:** - Drop support for Django 2.2 (EOL) (#567) - Added silk_request_garbage_collect command for out-of-band garbage collection. (#541) ## [4.4.1](https://github.com/jazzband/django-silk/tree/4.4.1) (2022-07-03) :release-by: Albert Wang (@albertyw) [Full Changelog](https://github.com/jazzband/django-silk/compare/4.4.0...4.4.1) **Fixes:** - Add jquery UI 1.13.1 images and fix collectstatic (#576) ## [4.4.0](https://github.com/jazzband/django-silk/tree/4.4.0) (2022-06-20) :release-by: Albert Wang (@albertyw) [Full Changelog](https://github.com/jazzband/django-silk/compare/4.3.0...4.4.0) **Features/Enhancements:** - Switch 'Apply' and 'Clear all filters' ordering - Make filters on Requests tab more visible - Add small margin for filter selects - Add 'Clear all filters' button - Add message when there are no requests to display - Making the error logging more accurate and explicit - Fixing #530 - Adding support for SILKY_EXPLAIN_FLAGS **Maintenance and Cleanup:** - Remove unused js compilation pipeline (#561) - Fix pre-commit-config **Dependencies:** - Update jquery to 3.6.0 and jquery-ui to 1.13.1 [#508] - [pre-commit.ci] pre-commit autoupdate (#560, #571) - Add django-upgrade to pre-commit hooks (#566) **Moved to 5.0.0** - Drop support for Django 2.2 (EOL) (#567) ## [4.3.0](https://github.com/jazzband/django-silk/tree/4.3.0) (2022-03-01) :release-by: Albert Wang (@albertyw) [Full Changelog](https://github.com/jazzband/django-silk/compare/4.2.0...4.3.0) **Fixes:** - Use correct db in a multi db setup (https://github.com/jazzband/django-silk/issues/522) **Dependencies:** - Drop support for Python 3.6 - Add Python 3.10 compatibility - Add Django 4.0 to tox.ini - Update django version (#544) - Django main (#528) - Remove unneeded dependency Pygments **Maintenance and Cleanup:** - Jazzband: Created local 'CODE_OF_CONDUCT.md' from remote 'CODE_OF_CONDUCT.md' - fix installation instructions in README - Replace assertDictContainsSubset (#536) - Fix issue avoid-misusing-assert-true found at https://codereview.doctor (#550) - pre-commit autoupdate ## [4.2.0](https://github.com/jazzband/django-silk/tree/4.2.0) (2021-23-10) :release-by: Asif Saif Uddin (@auvipy) [Full Changelog](https://github.com/jazzband/django-silk/compare/4.1.0...4.2.0) - #427 Passed wsgi request to SILKY_PYTHON_PROFILER_FUNC - Added Django 3.1 & 3.2 to test matrix - Replace url with re_path for Django 4.0 - Move CI to GitHub Actions. [\#460](https://github.com/jazzband/django-silk/pull/432) ([jezdez](https://github.com/jezdez)) - Do not crash when silk app is not included in urls - Add the SILKY_JSON_ENSURE_ASCII configuration item to support Chinese - Add row view for requests page (#440) - RequestModelFactory: fallback if request body too large, fix #162 (#451) - Add query execution plan to sql_detail (#452) - Add Python 3.9 compatibility (#404) - Replace re_path with path - Fix transaction error for mysql - parse query when count joins to match only Keyword - fix: DB connection to ClearDB when multiple databases - fix: DataCollector sql_queries model not found on filter(request=self.request) - Generate missing row.css from sass - Filter null values from most time overall summary - Ensure sorting between longest requests - Filter null values from most db time summary - Ensure sorting between most db time requests - Temporary fix for testing Django 2.2 - Fix egg metadata error - Fixed a bug that the profile tab could not be opened when the source code contains japanese - fix incorrectly made decorator - Ensure sorting between most db queries requests - Add tests that access the actual DB (#493) - remove python 2 style codes from across the codebase - Fix broken test on Windows 10 (SQLite) (#504) - Remove Make Migrations (#503) - Add Python 3.10 compatibility (#527) ## [4.1.0](https://github.com/jazzband/django-silk/tree/4.1.0) (2020-08-07) [Full Changelog](https://github.com/jazzband/django-silk/compare/4.0.1...4.1.0) **New features/Implemented enhancements:** - Make compatible with Django 3.1 [\#432](https://github.com/jazzband/django-silk/pull/432) ([Tirzono](https://github.com/Tirzono)) **Fixed bugs:** - Capture entire key name during cleansing in \_mask\_credentials [\#414](https://github.com/jazzband/django-silk/pull/414) ([ThePumpingLemma](https://github.com/ThePumpingLemma)) - Clear DB error when configuring silk to use a non-' default' database [\#417](https://github.com/jazzband/django-silk/pull/417) ([eshxcmhk](https://github.com/eshxcmhk)) - Fix force\_text RemovedInDjango40Warning [\#422](https://github.com/jazzband/django-silk/pull/422) ([justinmayhew](https://github.com/justinmayhew)) **Closed issues:** - \_mask\_credentials uses UGC in a regex substitution [\#410](https://github.com/jazzband/django-silk/issues/410) ([barm](https://github.com/barm)) - Django Silk is not compatible with Django 3.1: EmptyResultSet is removed in Django 3.1 [\#431](https://github.com/jazzband/django-silk/issues/431) ([Tirzono](https://github.com/Tirzono)) **Merged pull requests:** - Wrap re.sub() in try-except [\#412](https://github.com/jazzband/django-silk/pull/412) ([bambookchos](https://github.com/bambookchos)) - Replace the call to re.findall with re.sub in \_mask\_credentials so matched values are not treated as regex patterns [\#413](https://github.com/jazzband/django-silk/pull/413) ([ThePumpingLemma](https://github.com/ThePumpingLemma)) - Capture entire key name during cleansing in \_mask\_credentials [\#414](https://github.com/jazzband/django-silk/pull/414) ([ThePumpingLemma](https://github.com/ThePumpingLemma)) - Clear DB error when configuring silk to use a non-' default' database [\#417](https://github.com/jazzband/django-silk/pull/417) ([eshxcmhk](https://github.com/eshxcmhk)) - Fix force\_text RemovedInDjango40Warning [\#422](https://github.com/jazzband/django-silk/pull/422) ([justinmayhew](https://github.com/justinmayhew)) - Make compatible with Django 3.1 [\#432](https://github.com/jazzband/django-silk/pull/432) ([Tirzono](https://github.com/Tirzono)) - Update README.md django-silk is tested with Django 3.1 [\#433](https://github.com/jazzband/django-silk/pull/433) ([Tirzono](https://github.com/Tirzono)) ## [4.0.1](https://github.com/jazzband/django-silk/tree/4.0.1) (2020-03-12) [Full Changelog](https://github.com/jazzband/django-silk/compare/4.0.0...4.0.1) **New features/Implemented enhancements:** - Restructured clear db HTML [\#399](https://github.com/jazzband/django-silk/pull/399) ([nasirhjafri](https://github.com/nasirhjafri)) - JS workflow cleanup [\#397](https://github.com/jazzband/django-silk/pull/397) ([nasirhjafri](https://github.com/nasirhjafri)) - Refactor QA setup [\#393](https://github.com/jazzband/django-silk/pull/393) ([aleksihakli](https://github.com/aleksihakli)) **Fixed bugs:** - docs: Fix simple typo, tracebackk -> traceback [\#406](https://github.com/jazzband/django-silk/pull/406) ([timgates42](https://github.com/timgates42)) - Clear DB page doesn't work with PostgreSQL and SQLite [\#396](https://github.com/jazzband/django-silk/pull/396) ([nasirhjafri](https://github.com/nasirhjafri)) **Closed issues:** - The "Clear DB" page doesn't work with PostgreSQL [\#395](https://github.com/jazzband/django-silk/issues/395) ([Ikalou](https://github.com/Ikalou)) **Merged pull requests:** - docs: Fix simple typo, tracebackk -> traceback [\#406](https://github.com/jazzband/django-silk/pull/406) ([timgates42](https://github.com/timgates42)) - Restructured clear db HTML [\#399](https://github.com/jazzband/django-silk/pull/399) ([nasirhjafri](https://github.com/nasirhjafri)) - JS workflow cleanup [\#397](https://github.com/jazzband/django-silk/pull/397) ([nasirhjafri](https://github.com/nasirhjafri)) - Clear DB page doesn't work with PostgreSQL and SQLite [\#396](https://github.com/jazzband/django-silk/pull/396) ([nasirhjafri](https://github.com/nasirhjafri)) - Refactor QA setup [\#393](https://github.com/jazzband/django-silk/pull/393) ([aleksihakli](https://github.com/aleksihakli)) ## [4.0.0](https://github.com/jazzband/django-silk/tree/4.0.0) (2020-01-09) [Full Changelog](https://github.com/jazzband/django-silk/compare/3.0.4...4.0.0) **New features/Implemented enhancements:** - Ability to clean up all requests/queries [\#368](https://github.com/jazzband/django-silk/pull/368) ([nasirhjafri](https://github.com/nasirhjafri)) - Used bulk_create to save number of queries [\#370](https://github.com/jazzband/django-silk/pull/370) ([nasirhjafri](https://github.com/nasirhjafri)) - Dropped Python 2 and 3.4 support [\#380](https://github.com/jazzband/django-silk/pull/380) ([munza](https://github.com/munza)) - Added Python 3.8 support [\#380](https://github.com/jazzband/django-silk/pull/380) ([nasirhjafri](https://github.com/nasirhjafri)) - Removed django<2.2 support and added django 3.0 support [\#385](https://github.com/jazzband/django-silk/pull/385) ([nasirhjafri](https://github.com/nasirhjafri)) - Add function support for enabling profiling [\#391](https://github.com/jazzband/django-silk/pull/391) ([tredzko](https://github.com/tredzko)) **Fixed bugs:** - Mask authorization header [\#376](https://github.com/jazzband/django-silk/pull/376) ([StefanMich](https://github.com/StefanMich)) **Closed issues:** - Ability to clean up all requests/queries [\#365](https://github.com/jazzband/django-silk/issues/365) - Use bulk_create to save number of queries [\#369](https://github.com/jazzband/django-silk/issues/369) - Headers are not sanitized [\#375](https://github.com/jazzband/django-silk/issues/375) - Django 3 support [\#382](https://github.com/jazzband/django-silk/issues/382) - Support functional cProfile enable [\#390](https://github.com/jazzband/django-silk/issues/390) **Merged pull requests:** - Mask authorization header [\#376](https://github.com/jazzband/django-silk/pull/376) ([StefanMich](https://github.com/StefanMich)) - Ability to clean up all requests/queries [\#368](https://github.com/jazzband/django-silk/pull/368) ([nasirhjafri](https://github.com/nasirhjafri)) - Used bulk_create to save number of queries [\#370](https://github.com/jazzband/django-silk/pull/370) ([nasirhjafri](https://github.com/nasirhjafri)) - Dropped Python 2 and 3.4 support [\#380](https://github.com/jazzband/django-silk/pull/380) ([munza](https://github.com/munza)) - Added Python 3.8 support [\#380](https://github.com/jazzband/django-silk/pull/380) ([nasirhjafri](https://github.com/nasirhjafri)) - Removed django<2.2 support and added django 3.0 support [\#385](https://github.com/jazzband/django-silk/pull/385) ([nasirhjafri](https://github.com/nasirhjafri)) - Add function support for enabling profiling [\#391](https://github.com/jazzband/django-silk/pull/391) ([tredzko](https://github.com/tredzko)) ## [3.0.4](https://github.com/jazzband/django-silk/tree/3.0.4) (2019-08-12) [Full Changelog](https://github.com/jazzband/django-silk/compare/3.0.2...3.0.4) **Implemented enhancements:** - templates: limit select width to its container one [\#351](https://github.com/jazzband/django-silk/pull/351) ([xrmx](https://github.com/xrmx)) - Clean up RemovedInDjango30Warning with {% load staticfiles %} [\#353](https://github.com/jazzband/django-silk/pull/353) ([devmonkey22](https://github.com/devmonkey22)) - Simplify pattern masking and handle dicts [\#355](https://github.com/jazzband/django-silk/pull/355) ([Chris7](https://github.com/Chris7)) **Fixed bugs:** - Fix masking sensitive data in batch JSON request [\#342](https://github.com/jazzband/django-silk/pull/342) ([nikolaik](https://github.com/nikolaik)) - Fix project url on PyPi [\#343](https://github.com/jazzband/django-silk/pull/343) ([luzfcb](https://github.com/luzfcb)) **Closed issues:** - Clean up RemovedInDjango30Warning warning re `load staticfiles` in Django 2.1+ [\#352](https://github.com/jazzband/django-silk/issues/352) **Merged pull requests:** - Fix masking sensitive data in batch JSON request [\#342](https://github.com/jazzband/django-silk/pull/342) ([nikolaik](https://github.com/nikolaik)) - Fix project url on PyPi [\#343](https://github.com/jazzband/django-silk/pull/343) ([luzfcb](https://github.com/luzfcb)) - templates: limit select width to its container one [\#351](https://github.com/jazzband/django-silk/pull/351) ([xrmx](https://github.com/xrmx)) - Clean up RemovedInDjango30Warning with {% load staticfiles %} [\#353](https://github.com/jazzband/django-silk/pull/353) ([devmonkey22](https://github.com/devmonkey22)) - Simplify pattern masking and handle dicts [\#355](https://github.com/jazzband/django-silk/pull/355) ([Chris7](https://github.com/Chris7)) ## [3.0.2](https://github.com/jazzband/django-silk/tree/3.0.2) (2019-04-23) [Full Changelog](https://github.com/jazzband/django-silk/compare/3.0.1...3.0.2) **Implemented enhancements:** - Add testing support for django 2.2 [\#340](https://github.com/jazzband/django-silk/pull/340) ([mbeacom](https://github.com/mbeacom)) - SILKY\_MIDDLEWARE\_CLASS option [\#334](https://github.com/jazzband/django-silk/pull/334) ([vartagg](https://github.com/vartagg)) **Fixed bugs:** - Long url path causes Http 500 [\#312](https://github.com/jazzband/django-silk/issues/312) **Closed issues:** - Permission checking is skipped due to order of silk\_profile decorator [\#336](https://github.com/jazzband/django-silk/issues/336) - Support gprof2dot 2017.09.19 [\#332](https://github.com/jazzband/django-silk/issues/332) - Duplicate \#310 [\#328](https://github.com/jazzband/django-silk/issues/328) - Profiling management commands [\#327](https://github.com/jazzband/django-silk/issues/327) - NoReverseMatch at /cart/detail/ Reverse for 'cart\_add' with arguments not found. [\#324](https://github.com/jazzband/django-silk/issues/324) - Request body sanitization [\#305](https://github.com/jazzband/django-silk/issues/305) - How to profile middleware? [\#303](https://github.com/jazzband/django-silk/issues/303) - Disabling Silk for specific URLs [\#292](https://github.com/jazzband/django-silk/issues/292) - silk\_clear\_request\_log fails on Postgres [\#290](https://github.com/jazzband/django-silk/issues/290) - silk profile is not work, with dango-version 2.0.2 and django-silk version 2.0.0 [\#277](https://github.com/jazzband/django-silk/issues/277) - DataError: value too long for type character varying\(190\) [\#179](https://github.com/jazzband/django-silk/issues/179) **Merged pull requests:** - Update gprof2dot requirement [\#333](https://github.com/jazzband/django-silk/pull/333) ([Regzon](https://github.com/Regzon)) - Make Request.garbage\_collect cheaper [\#331](https://github.com/jazzband/django-silk/pull/331) ([xrmx](https://github.com/xrmx)) - Sort view filters values [\#330](https://github.com/jazzband/django-silk/pull/330) ([xrmx](https://github.com/xrmx)) - Update Travis CI matrix [\#326](https://github.com/jazzband/django-silk/pull/326) ([kevin-brown](https://github.com/kevin-brown)) - Fix unit for max response body size in readme [\#325](https://github.com/jazzband/django-silk/pull/325) ([st4lk](https://github.com/st4lk)) - Mask sensitive data [\#322](https://github.com/jazzband/django-silk/pull/322) ([egichuri](https://github.com/egichuri)) - Disclose security issues [\#321](https://github.com/jazzband/django-silk/pull/321) ([acu192](https://github.com/acu192)) - If there is no DataCollector\(\).request then don't wrap sql queries [\#320](https://github.com/jazzband/django-silk/pull/320) ([rwlogel](https://github.com/rwlogel)) - Prevent path or view\_name being longer than 190 characters [\#314](https://github.com/jazzband/django-silk/pull/314) ([smaccona](https://github.com/smaccona)) - Disable postgres USER triggers [\#299](https://github.com/jazzband/django-silk/pull/299) ([gforcada](https://github.com/gforcada)) - Fix \#297 remove explicit byte string from migration 0003 [\#298](https://github.com/jazzband/django-silk/pull/298) ([florianm](https://github.com/florianm)) - Modernize middleware [\#296](https://github.com/jazzband/django-silk/pull/296) ([gforcada](https://github.com/gforcada)) - Added a simple view in request detail context allowing to get python profile [\#295](https://github.com/jazzband/django-silk/pull/295) ([laurentb2](https://github.com/laurentb2)) ## [3.0.1](https://github.com/jazzband/django-silk/tree/3.0.1) (2018-07-03) [Full Changelog](https://github.com/jazzband/django-silk/compare/3.0.0...3.0.1) **Closed issues:** - ProgrammingError raised from silk\_clear\_request\_log [\#293](https://github.com/jazzband/django-silk/issues/293) - Make a new release of django-silk [\#282](https://github.com/jazzband/django-silk/issues/282) **Merged pull requests:** - \#290 Fix silk\_clear\_request\_log errors on Postgres [\#291](https://github.com/jazzband/django-silk/pull/291) ([devmonkey22](https://github.com/devmonkey22)) ## [3.0.0](https://github.com/jazzband/django-silk/tree/3.0.0) (2018-05-15) [Full Changelog](https://github.com/jazzband/django-silk/compare/2.0.0...3.0.0) **Implemented enhancements:** - Limiting request/response data don't available in pypi version [\#218](https://github.com/jazzband/django-silk/issues/218) **Fixed bugs:** - silk\_clear\_request\_log taking longer than 30 minutes [\#239](https://github.com/jazzband/django-silk/issues/239) **Closed issues:** - Meta profiling does not work with Django 2.0 and higher [\#274](https://github.com/jazzband/django-silk/issues/274) - Force opening a new window for SQL queries is very annoying [\#271](https://github.com/jazzband/django-silk/issues/271) - DB Deadlock when stress testing with silk [\#265](https://github.com/jazzband/django-silk/issues/265) - proplem with propagating code to pypi [\#264](https://github.com/jazzband/django-silk/issues/264) - PSA: Cleanup silk\_requests before updating to 1.1.0 [\#261](https://github.com/jazzband/django-silk/issues/261) - Release 2.0.0 [\#259](https://github.com/jazzband/django-silk/issues/259) **Merged pull requests:** - Remove gitter links [\#285](https://github.com/jazzband/django-silk/pull/285) ([albertyw](https://github.com/albertyw)) - Release 3.0.0 [\#283](https://github.com/jazzband/django-silk/pull/283) ([albertyw](https://github.com/albertyw)) - Fix garbage collection logic for small tables [\#280](https://github.com/jazzband/django-silk/pull/280) ([albertyw](https://github.com/albertyw)) - Fix view name [\#278](https://github.com/jazzband/django-silk/pull/278) ([drppi44](https://github.com/drppi44)) - Revert "Opening sql queries in new tab is very useful" [\#276](https://github.com/jazzband/django-silk/pull/276) ([albertyw](https://github.com/albertyw)) - Fix issue \#274 [\#275](https://github.com/jazzband/django-silk/pull/275) ([MKolman](https://github.com/MKolman)) - Truncate tables when running silk\_clear\_request\_log [\#270](https://github.com/jazzband/django-silk/pull/270) ([albertyw](https://github.com/albertyw)) - Makes example\_app.models.Product.photo.upload\_to a string instead of bytes [\#268](https://github.com/jazzband/django-silk/pull/268) ([vbawa](https://github.com/vbawa)) - Make garbage collection filter more efficient [\#267](https://github.com/jazzband/django-silk/pull/267) ([albertyw](https://github.com/albertyw)) - Drop support for Django \< 1.11 and remove workarounds [\#266](https://github.com/jazzband/django-silk/pull/266) ([jdufresne](https://github.com/jdufresne)) ## [2.0.0](https://github.com/jazzband/django-silk/tree/2.0.0) (2018-01-16) [Full Changelog](https://github.com/jazzband/django-silk/compare/1.1.0...2.0.0) **Fixed bugs:** - Links for Readme.md not working. [\#250](https://github.com/jazzband/django-silk/issues/250) **Closed issues:** - pypi version [\#252](https://github.com/jazzband/django-silk/issues/252) - Remove support for django 1.7 [\#247](https://github.com/jazzband/django-silk/issues/247) - migrations/0005\_increase\_request\_prof\_file\_length.py does not match code [\#244](https://github.com/jazzband/django-silk/issues/244) - Excessive number of queries in class method profile [\#240](https://github.com/jazzband/django-silk/issues/240) - Django 2.0 support [\#229](https://github.com/jazzband/django-silk/issues/229) - Create new release of silk [\#187](https://github.com/jazzband/django-silk/issues/187) **Merged pull requests:** - Release 2.0.0 [\#260](https://github.com/jazzband/django-silk/pull/260) ([albertyw](https://github.com/albertyw)) - function declaration fix [\#254](https://github.com/jazzband/django-silk/pull/254) ([Yolley](https://github.com/Yolley)) - Opening sql queries in new tab is very useful [\#253](https://github.com/jazzband/django-silk/pull/253) ([lokeshatbigbasket](https://github.com/lokeshatbigbasket)) - Use force\_text in ResponseModelFactory to avoid b' prefix in django 2 [\#251](https://github.com/jazzband/django-silk/pull/251) ([aadu](https://github.com/aadu)) - Remove django support 1.7 [\#249](https://github.com/jazzband/django-silk/pull/249) ([albertyw](https://github.com/albertyw)) - Remove django 1.6 references [\#248](https://github.com/jazzband/django-silk/pull/248) ([albertyw](https://github.com/albertyw)) - Update development status and python support to package classifiers [\#246](https://github.com/jazzband/django-silk/pull/246) ([albertyw](https://github.com/albertyw)) - fix migration for request.prof\_file field [\#245](https://github.com/jazzband/django-silk/pull/245) ([dennybiasiolli](https://github.com/dennybiasiolli)) - fix alternative github tags installation url [\#243](https://github.com/jazzband/django-silk/pull/243) ([dennybiasiolli](https://github.com/dennybiasiolli)) ## [1.1.0](https://github.com/jazzband/django-silk/tree/1.1.0) (2017-12-27) [Full Changelog](https://github.com/jazzband/django-silk/compare/1.0.0...1.1.0) **Implemented enhancements:** - RemovedInDjango20Warning: on\_delete will be a required arg for OneToOneField in Django 2.0. [\#183](https://github.com/jazzband/django-silk/issues/183) - README missing info about how to import decorator [\#180](https://github.com/jazzband/django-silk/issues/180) - Use redis for backend [\#163](https://github.com/jazzband/django-silk/issues/163) - Difficult to install on windows: Needs wheels. [\#149](https://github.com/jazzband/django-silk/issues/149) - Organise cProfile output as a sortable, more organised table. [\#33](https://github.com/jazzband/django-silk/issues/33) **Closed issues:** - Silk is incompatible with django-fullclean [\#219](https://github.com/jazzband/django-silk/issues/219) - The dashboard shows views with no queries as most time taken in database [\#217](https://github.com/jazzband/django-silk/issues/217) - No end\_time for any captured request [\#213](https://github.com/jazzband/django-silk/issues/213) - Bad alignment in profile table [\#206](https://github.com/jazzband/django-silk/issues/206) - Visualization not visible [\#205](https://github.com/jazzband/django-silk/issues/205) - Storage class as a setting [\#202](https://github.com/jazzband/django-silk/issues/202) - Consider moving project to jazzband [\#184](https://github.com/jazzband/django-silk/issues/184) - Request detail page never loads [\#175](https://github.com/jazzband/django-silk/issues/175) - Number of queries and time showing as 0 [\#174](https://github.com/jazzband/django-silk/issues/174) - NameError: name 'silk\_profile' is not defined [\#172](https://github.com/jazzband/django-silk/issues/172) - Query time-outs [\#158](https://github.com/jazzband/django-silk/issues/158) **Merged pull requests:** - Release 1.1.0 [\#242](https://github.com/jazzband/django-silk/pull/242) ([albertyw](https://github.com/albertyw)) - Update package versions for test project [\#241](https://github.com/jazzband/django-silk/pull/241) ([albertyw](https://github.com/albertyw)) - Return immediately [\#235](https://github.com/jazzband/django-silk/pull/235) ([Stranger6667](https://github.com/Stranger6667)) - Fix missing db\_time field [\#234](https://github.com/jazzband/django-silk/pull/234) ([albertyw](https://github.com/albertyw)) - Test django 2 in travis [\#233](https://github.com/jazzband/django-silk/pull/233) ([albertyw](https://github.com/albertyw)) - Lint silk directory and fix a python 3 blocker [\#232](https://github.com/jazzband/django-silk/pull/232) ([albertyw](https://github.com/albertyw)) - Fix flaky test by rounding off floats [\#231](https://github.com/jazzband/django-silk/pull/231) ([albertyw](https://github.com/albertyw)) - Fix github silk links to point to jazzband [\#230](https://github.com/jazzband/django-silk/pull/230) ([albertyw](https://github.com/albertyw)) - Update docs to clarify how to install the middleware [\#228](https://github.com/jazzband/django-silk/pull/228) ([albertyw](https://github.com/albertyw)) - Fix Django 2 deprecations [\#227](https://github.com/jazzband/django-silk/pull/227) ([albertyw](https://github.com/albertyw)) - Add extra documentation covering environment variables and running tests [\#226](https://github.com/jazzband/django-silk/pull/226) ([richardnias](https://github.com/richardnias)) - Filter out views that took no time in the database for the most time … [\#225](https://github.com/jazzband/django-silk/pull/225) ([hvdklauw](https://github.com/hvdklauw)) - Removed typo errors and fixed contractions [\#222](https://github.com/jazzband/django-silk/pull/222) ([basifat](https://github.com/basifat)) - gprof2dot had a breaking change in 2017.09.19 [\#221](https://github.com/jazzband/django-silk/pull/221) ([richardnias](https://github.com/richardnias)) - Allow prof\_file to be blank, not null [\#220](https://github.com/jazzband/django-silk/pull/220) ([richardnias](https://github.com/richardnias)) - Changed the theme of gprof2dot output to be more inline with rest of silk design [\#210](https://github.com/jazzband/django-silk/pull/210) ([danielbradburn](https://github.com/danielbradburn)) - configurable storage class [\#204](https://github.com/jazzband/django-silk/pull/204) ([smcoll](https://github.com/smcoll)) - increase Request.prof\_file max\_length to 300 [\#203](https://github.com/jazzband/django-silk/pull/203) ([smcoll](https://github.com/smcoll)) - \#33 organise cprofile output as a sortable table [\#200](https://github.com/jazzband/django-silk/pull/200) ([danielbradburn](https://github.com/danielbradburn)) - left align pre tag text [\#199](https://github.com/jazzband/django-silk/pull/199) ([smcoll](https://github.com/smcoll)) - add .venv\* to .gitignore [\#198](https://github.com/jazzband/django-silk/pull/198) ([danielbradburn](https://github.com/danielbradburn)) - Add missing gprof2dot to setup.py [\#197](https://github.com/jazzband/django-silk/pull/197) ([danielbradburn](https://github.com/danielbradburn)) - README changes for visualisation and sql summary table sorting [\#195](https://github.com/jazzband/django-silk/pull/195) ([danielbradburn](https://github.com/danielbradburn)) - Added UI element to filter requests by http verb [\#194](https://github.com/jazzband/django-silk/pull/194) ([danielbradburn](https://github.com/danielbradburn)) - Sortable sql table [\#193](https://github.com/jazzband/django-silk/pull/193) ([danielbradburn](https://github.com/danielbradburn)) - Visualize profile result [\#192](https://github.com/jazzband/django-silk/pull/192) ([danielbradburn](https://github.com/danielbradburn)) - Added status code filter [\#191](https://github.com/jazzband/django-silk/pull/191) ([danielbradburn](https://github.com/danielbradburn)) - Set jazzband to limit the number of rows of request/response data [\#190](https://github.com/jazzband/django-silk/pull/190) ([albertyw](https://github.com/albertyw)) - Add python 3.6 to travis config [\#189](https://github.com/jazzband/django-silk/pull/189) ([albertyw](https://github.com/albertyw)) - Add explicit on\_delete to foreign key and one to one relationships [\#188](https://github.com/jazzband/django-silk/pull/188) ([albertyw](https://github.com/albertyw)) - Replace django-silk organization with jazzband [\#186](https://github.com/jazzband/django-silk/pull/186) ([albertyw](https://github.com/albertyw)) - Jazzband migration [\#185](https://github.com/jazzband/django-silk/pull/185) ([mtford90](https://github.com/mtford90)) - Deprecation: update to warning [\#177](https://github.com/jazzband/django-silk/pull/177) ([lammertw](https://github.com/lammertw)) - Add text-align property to pyprofile class for readability [\#176](https://github.com/jazzband/django-silk/pull/176) ([jeffreyckchau](https://github.com/jeffreyckchau)) - Mention collectstatic [\#173](https://github.com/jazzband/django-silk/pull/173) ([goetzk](https://github.com/goetzk)) ## [1.0.0](https://github.com/jazzband/django-silk/tree/1.0.0) (2017-03-25) [Full Changelog](https://github.com/jazzband/django-silk/compare/0.7.3...1.0.0) **Fixed bugs:** - Silk shows 0 time for all requests? [\#161](https://github.com/jazzband/django-silk/issues/161) - Failed to install index for silk.Request model: \(1071, 'Specified key was too long; max key length is 767 bytes'\) [\#38](https://github.com/jazzband/django-silk/issues/38) - IntegrityError: duplicate key value violates unique constraint "silk\_response\_request\_id\_key" [\#26](https://github.com/jazzband/django-silk/issues/26) **Closed issues:** - There is no reference to download a profile [\#170](https://github.com/jazzband/django-silk/issues/170) - Build fails occasionally due to "missing manage.py" [\#32](https://github.com/jazzband/django-silk/issues/32) **Merged pull requests:** - Fixes \#170 [\#171](https://github.com/jazzband/django-silk/pull/171) ([perdy](https://github.com/perdy)) - Wheel support [\#168](https://github.com/jazzband/django-silk/pull/168) ([auvipy](https://github.com/auvipy)) - Improved MySQL support [\#167](https://github.com/jazzband/django-silk/pull/167) ([smaccona](https://github.com/smaccona)) - some style improvements [\#166](https://github.com/jazzband/django-silk/pull/166) ([auvipy](https://github.com/auvipy)) - Update travis matrix and requirments dependencies versions [\#165](https://github.com/jazzband/django-silk/pull/165) ([auvipy](https://github.com/auvipy)) - Fixes \#161 [\#164](https://github.com/jazzband/django-silk/pull/164) ([perdy](https://github.com/perdy)) ## [0.7.3](https://github.com/jazzband/django-silk/tree/0.7.3) (2017-02-13) [Full Changelog](https://github.com/jazzband/django-silk/compare/0.7.2...0.7.3) **Fixed bugs:** - Profiling files get copied into MEDIA\_ROOT [\#151](https://github.com/jazzband/django-silk/issues/151) - Bad requirements for postgres based installations [\#142](https://github.com/jazzband/django-silk/issues/142) **Closed issues:** - Middleware setting in Django 1.10 [\#159](https://github.com/jazzband/django-silk/issues/159) - When installing silk asking for mysql library. But I'm using postgresql. [\#150](https://github.com/jazzband/django-silk/issues/150) - No Silk profiling was performed for this request. Use the silk\_profile decorator/context manager to do so. [\#147](https://github.com/jazzband/django-silk/issues/147) - ProgrammingError on postgresql [\#146](https://github.com/jazzband/django-silk/issues/146) - \[Error\]\[Bug\]adding silk middleware in MIDDLEWARE causes ImportError [\#108](https://github.com/jazzband/django-silk/issues/108) **Merged pull requests:** - Update middleware setting for Django \>= 1.10 [\#160](https://github.com/jazzband/django-silk/pull/160) ([ukjin1192](https://github.com/ukjin1192)) - Add favorite icons [\#156](https://github.com/jazzband/django-silk/pull/156) ([phuong](https://github.com/phuong)) - Bugfix for issue \#153 [\#155](https://github.com/jazzband/django-silk/pull/155) ([Drache91](https://github.com/Drache91)) - Improve profile storage [\#152](https://github.com/jazzband/django-silk/pull/152) ([r3m0t](https://github.com/r3m0t)) ## [0.7.2](https://github.com/jazzband/django-silk/tree/0.7.2) (2016-12-03) [Full Changelog](https://github.com/jazzband/django-silk/compare/0.7.1...0.7.2) **Closed issues:** - Pypi version upload [\#141](https://github.com/jazzband/django-silk/issues/141) **Merged pull requests:** - Allow using Django 1.10 MIDDLEWARE setting instead of MIDDLEWARE\_CLASSES [\#148](https://github.com/jazzband/django-silk/pull/148) ([lockie](https://github.com/lockie)) - Travis config to test on the different django database backends. [\#145](https://github.com/jazzband/django-silk/pull/145) ([mattjegan](https://github.com/mattjegan)) - Updates exception handling to use Django DatabaseError class [\#144](https://github.com/jazzband/django-silk/pull/144) ([hanleyhansen](https://github.com/hanleyhansen)) - Fix for byte string incompatibility in ResponseModelFactory.body\(\) on py3 [\#143](https://github.com/jazzband/django-silk/pull/143) ([aljp](https://github.com/aljp)) ## [0.7.1](https://github.com/jazzband/django-silk/tree/0.7.1) (2016-10-01) [Full Changelog](https://github.com/jazzband/django-silk/compare/0.7.0...0.7.1) **Merged pull requests:** - Operational Error When Silk Is Used On Big SQL Queries [\#140](https://github.com/jazzband/django-silk/pull/140) ([hanleyhansen](https://github.com/hanleyhansen)) ## [0.7.0](https://github.com/jazzband/django-silk/tree/0.7.0) (2016-09-21) [Full Changelog](https://github.com/jazzband/django-silk/compare/0.6.2...0.7.0) **Implemented enhancements:** - Select a path to save profiling files [\#131](https://github.com/jazzband/django-silk/issues/131) **Merged pull requests:** - Remove trailing slashes in MANIFEST.in [\#139](https://github.com/jazzband/django-silk/pull/139) ([leifdenby](https://github.com/leifdenby)) - Django 1.10 compatibility [\#138](https://github.com/jazzband/django-silk/pull/138) ([shanx](https://github.com/shanx)) - Swap imports to avoid emitting warnings [\#136](https://github.com/jazzband/django-silk/pull/136) ([blag](https://github.com/blag)) - Profiler files path configurable [\#135](https://github.com/jazzband/django-silk/pull/135) ([javaguirre](https://github.com/javaguirre)) - Fix ignored content body [\#134](https://github.com/jazzband/django-silk/pull/134) ([aehlke](https://github.com/aehlke)) - Namespaced loggers [\#133](https://github.com/jazzband/django-silk/pull/133) ([aehlke](https://github.com/aehlke)) ## [0.6.2](https://github.com/jazzband/django-silk/tree/0.6.2) (2016-07-28) [Full Changelog](https://github.com/jazzband/django-silk/compare/0.6.1...0.6.2) **Closed issues:** - SnakeViz integration [\#83](https://github.com/jazzband/django-silk/issues/83) **Merged pull requests:** - don't crash when a route is 404 [\#129](https://github.com/jazzband/django-silk/pull/129) ([chrono](https://github.com/chrono)) ## [0.6.1](https://github.com/jazzband/django-silk/tree/0.6.1) (2016-07-13) [Full Changelog](https://github.com/jazzband/django-silk/compare/0.6.0...0.6.1) **Closed issues:** - Latest version of django-silk not installing because of missing dependency [\#127](https://github.com/jazzband/django-silk/issues/127) - README.md missing in v0.6 [\#125](https://github.com/jazzband/django-silk/issues/125) **Merged pull requests:** - use any readme [\#128](https://github.com/jazzband/django-silk/pull/128) ([SzySteve](https://github.com/SzySteve)) ## [0.6.0](https://github.com/jazzband/django-silk/tree/0.6.0) (2016-07-12) [Full Changelog](https://github.com/jazzband/django-silk/compare/0.5.7...0.6.0) **Closed issues:** - Local Dev of Silk. Template Error. [\#121](https://github.com/jazzband/django-silk/issues/121) - Using django six rather then maintaining one [\#112](https://github.com/jazzband/django-silk/issues/112) - PyPi release [\#106](https://github.com/jazzband/django-silk/issues/106) **Merged pull requests:** - update pillow requirement so installation succeeds [\#124](https://github.com/jazzband/django-silk/pull/124) ([SzySteve](https://github.com/SzySteve)) - Give users the ability to export .prof binary files for every request [\#123](https://github.com/jazzband/django-silk/pull/123) ([hanleyhansen](https://github.com/hanleyhansen)) - Make Silk Great Again and Upgrade Dev Project [\#122](https://github.com/jazzband/django-silk/pull/122) ([hanleyhansen](https://github.com/hanleyhansen)) - make file paths clickable that don't start with a slash [\#120](https://github.com/jazzband/django-silk/pull/120) ([chrono](https://github.com/chrono)) - clear data store in chunks [\#119](https://github.com/jazzband/django-silk/pull/119) ([chrono](https://github.com/chrono)) - remove claim to support django 1.6 [\#118](https://github.com/jazzband/django-silk/pull/118) ([chrono](https://github.com/chrono)) - removed six six utils and tests [\#117](https://github.com/jazzband/django-silk/pull/117) ([auvipy](https://github.com/auvipy)) - used django utils six instead of sils utls six in some module [\#116](https://github.com/jazzband/django-silk/pull/116) ([auvipy](https://github.com/auvipy)) - Lint fix and code cleaning [\#114](https://github.com/jazzband/django-silk/pull/114) ([auvipy](https://github.com/auvipy)) - small updates [\#113](https://github.com/jazzband/django-silk/pull/113) ([auvipy](https://github.com/auvipy)) - Render function instead of render\_to\_response [\#111](https://github.com/jazzband/django-silk/pull/111) ([auvipy](https://github.com/auvipy)) - remove south migrations as not needed in less then 1.7 [\#110](https://github.com/jazzband/django-silk/pull/110) ([auvipy](https://github.com/auvipy)) - versions upgrade and obsolete versions removal [\#109](https://github.com/jazzband/django-silk/pull/109) ([auvipy](https://github.com/auvipy)) - Supporting django\<1.8 [\#107](https://github.com/jazzband/django-silk/pull/107) ([wm3ndez](https://github.com/wm3ndez)) ## [0.5.7](https://github.com/jazzband/django-silk/tree/0.5.7) (2016-03-16) [Full Changelog](https://github.com/jazzband/django-silk/compare/0.5.6...0.5.7) **Implemented enhancements:** - Unittesting [\#87](https://github.com/jazzband/django-silk/issues/87) - Add Ascending/Descending sort order GET parameter in RequestsView [\#84](https://github.com/jazzband/django-silk/issues/84) - Support binary response bodies [\#1](https://github.com/jazzband/django-silk/issues/1) **Fixed bugs:** - TemplateSyntaxError at /silk/ Invalid filter: 'silk\_date\_time' [\#82](https://github.com/jazzband/django-silk/issues/82) **Closed issues:** - base64 encoded responses break unit tests for Python3 [\#98](https://github.com/jazzband/django-silk/issues/98) - Refactor Unit Tests to test new sort ordering structure. [\#96](https://github.com/jazzband/django-silk/issues/96) - Running tests from the Travis config file fails because of difference in django-admin/manage.py [\#91](https://github.com/jazzband/django-silk/issues/91) - Support for missing URL names in Django 1.8 and 1.9 [\#89](https://github.com/jazzband/django-silk/issues/89) - UnicodeDecodeError in sql.py: leads to 500 internal error [\#85](https://github.com/jazzband/django-silk/issues/85) **Merged pull requests:** - remove simplejson [\#105](https://github.com/jazzband/django-silk/pull/105) ([digitaldavenyc](https://github.com/digitaldavenyc)) - Fixing Depreciation, Saving and Performance Tweaks [\#104](https://github.com/jazzband/django-silk/pull/104) ([Wrhector](https://github.com/Wrhector)) - Django 1.9 compatibility for the csrf context processor [\#100](https://github.com/jazzband/django-silk/pull/100) ([blag](https://github.com/blag)) - URL patterns are just Python lists for Django 1.9+ [\#99](https://github.com/jazzband/django-silk/pull/99) ([blag](https://github.com/blag)) - Refactor Unit Tests to test new sort ordering structure. [\#97](https://github.com/jazzband/django-silk/pull/97) ([trik](https://github.com/trik)) - Add Ascending/Descending sort order GET parameter in RequestsView [\#95](https://github.com/jazzband/django-silk/pull/95) ([trik](https://github.com/trik)) - Response bodies are now stored b64 encoded \(support for binary responses\). [\#94](https://github.com/jazzband/django-silk/pull/94) ([trik](https://github.com/trik)) - Unittests for models [\#93](https://github.com/jazzband/django-silk/pull/93) ([Alkalit](https://github.com/Alkalit)) - Conditional migration tests [\#92](https://github.com/jazzband/django-silk/pull/92) ([florisdenhengst](https://github.com/florisdenhengst)) - Added support for missing URL names in Django 1.8-1.9. [\#90](https://github.com/jazzband/django-silk/pull/90) ([florisdenhengst](https://github.com/florisdenhengst)) - Avoid errors when doing migrate command [\#86](https://github.com/jazzband/django-silk/pull/86) ([msaelices](https://github.com/msaelices)) - Namespace templatetags so they don't clash with existing application templatetags [\#81](https://github.com/jazzband/django-silk/pull/81) ([lmortimer](https://github.com/lmortimer)) - Added the use of Lambdas in settings.py to the README. [\#77](https://github.com/jazzband/django-silk/pull/77) ([bryson](https://github.com/bryson)) ## [0.5.6](https://github.com/jazzband/django-silk/tree/0.5.6) (2015-09-06) [Full Changelog](https://github.com/jazzband/django-silk/compare/0.5.5...0.5.6) **Closed issues:** - Post-processing static assets fails due to missing font files [\#51](https://github.com/jazzband/django-silk/issues/51) **Merged pull requests:** - Fixed report handling timing not included in meta-timing [\#76](https://github.com/jazzband/django-silk/pull/76) ([rodcloutier](https://github.com/rodcloutier)) - Support UUID in request headers [\#75](https://github.com/jazzband/django-silk/pull/75) ([rodcloutier](https://github.com/rodcloutier)) - test on latest django versions in travis [\#72](https://github.com/jazzband/django-silk/pull/72) ([nikolas](https://github.com/nikolas)) ## [0.5.5](https://github.com/jazzband/django-silk/tree/0.5.5) (2015-06-04) [Full Changelog](https://github.com/jazzband/django-silk/compare/0.5.3...0.5.5) **Fixed bugs:** - Pin six.py within silk to avoid version incompatibility. [\#70](https://github.com/jazzband/django-silk/issues/70) **Closed issues:** - IntegrityError: NOT NULL constraint failed: silk\_request.view\_name [\#71](https://github.com/jazzband/django-silk/issues/71) ## [0.5.3](https://github.com/jazzband/django-silk/tree/0.5.3) (2015-06-04) [Full Changelog](https://github.com/jazzband/django-silk/compare/0.5.2...0.5.3) **Closed issues:** - null value in column "view\_name" violates not-null constraint [\#66](https://github.com/jazzband/django-silk/issues/66) - Migrations do not work with Django 1.5.9 [\#64](https://github.com/jazzband/django-silk/issues/64) **Merged pull requests:** - It's not random, is it? [\#69](https://github.com/jazzband/django-silk/pull/69) ([peterbe](https://github.com/peterbe)) - Fix issue when view\_name was Null [\#67](https://github.com/jazzband/django-silk/pull/67) ([bartoszhernas](https://github.com/bartoszhernas)) ## [0.5.2](https://github.com/jazzband/django-silk/tree/0.5.2) (2015-04-15) [Full Changelog](https://github.com/jazzband/django-silk/compare/0.5.1...0.5.2) **Merged pull requests:** - Update model\_factory.py [\#62](https://github.com/jazzband/django-silk/pull/62) ([karabijavad](https://github.com/karabijavad)) ## [0.5.1](https://github.com/jazzband/django-silk/tree/0.5.1) (2015-04-08) [Full Changelog](https://github.com/jazzband/django-silk/compare/0.5...0.5.1) **Implemented enhancements:** - UTC time in templates [\#49](https://github.com/jazzband/django-silk/issues/49) **Fixed bugs:** - AttributeError: This StreamingHttpResponse instance has no `content` attribute [\#50](https://github.com/jazzband/django-silk/issues/50) **Closed issues:** - Django 1.8 support [\#55](https://github.com/jazzband/django-silk/issues/55) - Should not have to manually add a logger for silk [\#53](https://github.com/jazzband/django-silk/issues/53) ## [0.5](https://github.com/jazzband/django-silk/tree/0.5) (2015-04-08) [Full Changelog](https://github.com/jazzband/django-silk/compare/v0.4...0.5) **Implemented enhancements:** - 'thread.\_local' object has no attribute 'temp\_identifier' \(should log a warning stating that this is likely a middleware issue\) [\#52](https://github.com/jazzband/django-silk/issues/52) - Check to see if process\_request of SilkyMiddleware has been called, and issue warnings on middleware placement if not [\#42](https://github.com/jazzband/django-silk/issues/42) - Django 1.7 support [\#29](https://github.com/jazzband/django-silk/issues/29) **Fixed bugs:** - Django 1.5 support broken [\#60](https://github.com/jazzband/django-silk/issues/60) **Closed issues:** - Tests broken [\#61](https://github.com/jazzband/django-silk/issues/61) - Deploying silk site-wide [\#56](https://github.com/jazzband/django-silk/issues/56) - Migration error [\#54](https://github.com/jazzband/django-silk/issues/54) - Silky doesn't work when django.middleware.gzip.GZipMiddleware is enabled [\#43](https://github.com/jazzband/django-silk/issues/43) - static files not found problem [\#41](https://github.com/jazzband/django-silk/issues/41) - No handlers could be found for logger "silk" [\#35](https://github.com/jazzband/django-silk/issues/35) **Merged pull requests:** - Add configuration option for custom intercept logic. [\#59](https://github.com/jazzband/django-silk/pull/59) ([kkaehler](https://github.com/kkaehler)) - commit\_on\_success -\> atomic, for 1.8, as commit\_on\_success was removed [\#58](https://github.com/jazzband/django-silk/pull/58) ([karabijavad](https://github.com/karabijavad)) - Update README.md [\#57](https://github.com/jazzband/django-silk/pull/57) ([karabijavad](https://github.com/karabijavad)) - Add a Gitter chat badge to README.md [\#48](https://github.com/jazzband/django-silk/pull/48) ([gitter-badger](https://github.com/gitter-badger)) - Tox integration added [\#47](https://github.com/jazzband/django-silk/pull/47) ([brmc](https://github.com/brmc)) - Edited ReadMe.md to avoid UnicodeDevodeError [\#44](https://github.com/jazzband/django-silk/pull/44) ([brmc](https://github.com/brmc)) - Added utf8 in curl query parameters [\#39](https://github.com/jazzband/django-silk/pull/39) ([ilvar](https://github.com/ilvar)) - Revert "Fix errors in manifest file" [\#37](https://github.com/jazzband/django-silk/pull/37) ([mtford90](https://github.com/mtford90)) - Fix IntegrityError caused by Request being saved 'None' raw\_body [\#36](https://github.com/jazzband/django-silk/pull/36) ([JannKleen](https://github.com/JannKleen)) - Fix errors in manifest file [\#34](https://github.com/jazzband/django-silk/pull/34) ([joaofrancese](https://github.com/joaofrancese)) ## [v0.4](https://github.com/jazzband/django-silk/tree/v0.4) (2014-08-17) [Full Changelog](https://github.com/jazzband/django-silk/compare/0.3.2...v0.4) **Closed issues:** - Live demo link is broken [\#30](https://github.com/jazzband/django-silk/issues/30) **Merged pull requests:** - Ability to not log every request, optimizations, db\_index, and a management command [\#31](https://github.com/jazzband/django-silk/pull/31) ([JoshData](https://github.com/JoshData)) ## [0.3.2](https://github.com/jazzband/django-silk/tree/0.3.2) (2014-07-22) [Full Changelog](https://github.com/jazzband/django-silk/compare/0.3.1...0.3.2) **Fixed bugs:** - No data profiled [\#25](https://github.com/jazzband/django-silk/issues/25) - Incorrect interface for execute\_sql [\#24](https://github.com/jazzband/django-silk/issues/24) **Closed issues:** - Don't pin versions in setup.py [\#23](https://github.com/jazzband/django-silk/issues/23) - Ability to clear old runs [\#14](https://github.com/jazzband/django-silk/issues/14) **Merged pull requests:** - Added tests for \_should\_intercept and fixed bug with requests not being ... [\#28](https://github.com/jazzband/django-silk/pull/28) ([mackeian](https://github.com/mackeian)) - Added missing requirement for running tests: mock [\#27](https://github.com/jazzband/django-silk/pull/27) ([mackeian](https://github.com/mackeian)) ## [0.3.1](https://github.com/jazzband/django-silk/tree/0.3.1) (2014-07-05) [Full Changelog](https://github.com/jazzband/django-silk/compare/0.3...0.3.1) **Implemented enhancements:** - Conform to charset flag in Content-Type header of request/response [\#20](https://github.com/jazzband/django-silk/issues/20) - Enhance filtering [\#17](https://github.com/jazzband/django-silk/issues/17) **Fixed bugs:** - Conform to charset flag in Content-Type header of request/response [\#20](https://github.com/jazzband/django-silk/issues/20) - HttpRequest body has UTF-8 Character causes UnicodeDecodeError ? [\#19](https://github.com/jazzband/django-silk/issues/19) **Closed issues:** - Problems with `six.moves.urllib` [\#22](https://github.com/jazzband/django-silk/issues/22) - Incorrect string value: '\xCE\xBB, \xCF\x86...' for column 'raw\_body' at row 1 [\#21](https://github.com/jazzband/django-silk/issues/21) - Silk fails on binary staticfiles content [\#16](https://github.com/jazzband/django-silk/issues/16) - Silk's static assets are served from the wrong path [\#11](https://github.com/jazzband/django-silk/issues/11) ## [0.3](https://github.com/jazzband/django-silk/tree/0.3) (2014-06-17) [Full Changelog](https://github.com/jazzband/django-silk/compare/V0.2.2...0.3) ## [V0.2.2](https://github.com/jazzband/django-silk/tree/V0.2.2) (2014-06-13) [Full Changelog](https://github.com/jazzband/django-silk/compare/v0.2.2...V0.2.2) ## [v0.2.2](https://github.com/jazzband/django-silk/tree/v0.2.2) (2014-06-13) [Full Changelog](https://github.com/jazzband/django-silk/compare/v0.2...v0.2.2) **Closed issues:** - request: timestamp on list of requests [\#15](https://github.com/jazzband/django-silk/issues/15) - AttributeError: 'thread.\_local' object has no attribute 'temp\_identifier' [\#12](https://github.com/jazzband/django-silk/issues/12) ## [v0.2](https://github.com/jazzband/django-silk/tree/v0.2) (2014-06-12) [Full Changelog](https://github.com/jazzband/django-silk/compare/v0.1.1...v0.2) **Fixed bugs:** - Stacktrace inspector allows users to see any file on the filesystem [\#10](https://github.com/jazzband/django-silk/issues/10) ## [v0.1.1](https://github.com/jazzband/django-silk/tree/v0.1.1) (2014-06-07) [Full Changelog](https://github.com/jazzband/django-silk/compare/v0.1...v0.1.1) **Closed issues:** - Pip install direct from repo fails [\#9](https://github.com/jazzband/django-silk/issues/9) - urls.py uses incorrect regex expressions [\#7](https://github.com/jazzband/django-silk/issues/7) - requirements.txt must specify exact versions or version upper bounds [\#6](https://github.com/jazzband/django-silk/issues/6) - Switch to PyPI for managing releases [\#4](https://github.com/jazzband/django-silk/issues/4) **Merged pull requests:** - Ensure README file is properly closed by setup.py [\#8](https://github.com/jazzband/django-silk/pull/8) ([svisser](https://github.com/svisser)) - updated readme [\#5](https://github.com/jazzband/django-silk/pull/5) ([rosscdh](https://github.com/rosscdh)) ## [v0.1](https://github.com/jazzband/django-silk/tree/v0.1) (2014-06-06) \* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)*⏎ ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # 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/ ================================================ FILE: CONTRIBUTING.md ================================================ [![Jazzband](https://jazzband.co/static/img/jazzband.svg)](https://jazzband.co/) This is a [Jazzband](https://jazzband.co/) project. By contributing you agree to abide by the [Contributor Code of Conduct](https://jazzband.co/about/conduct) and follow the [guidelines](https://jazzband.co/about/guidelines). ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2014 Michael Ford Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: MANIFEST.in ================================================ include LICENSE include README* recursive-include silk/templates * recursive-include silk/static * recursive-include silk/code_generation *.py recursive-include silk/profiling *.py recursive-include silk/utils *.py recursive-include silk/views *.py recursive-include silk *.py ================================================ FILE: README.md ================================================ # Silk [![GitHub Actions](https://github.com/jazzband/django-silk/workflows/Test/badge.svg)](https://github.com/jazzband/django-silk/actions) [![GitHub Actions](https://codecov.io/gh/jazzband/django-silk/branch/master/graph/badge.svg)](https://codecov.io/gh/jazzband/django-silk) [![PyPI Download](https://img.shields.io/pypi/v/django-silk.svg)](https://pypi.python.org/pypi/django-silk) [![PyPI Python Versions](https://img.shields.io/pypi/pyversions/django-silk.svg)](https://pypi.python.org/pypi/django-silk) [![Supported Django versions](https://img.shields.io/pypi/djversions/django-silk.svg)](https://pypi.python.org/pypi/django-silk) [![Jazzband](https://jazzband.co/static/img/badge.svg)](https://jazzband.co/) Silk is a live profiling and inspection tool for the Django framework. Silk intercepts and stores HTTP requests and database queries before presenting them in a user interface for further inspection: ## Contents * [Requirements](#requirements) * [Installation](#installation) * [Features](#features) * [Configuration](#configuration) * [Authentication/Authorisation](#authenticationauthorisation) * [Request/Response bodies](#requestresponse-bodies) * [Meta-Profiling](#meta-profiling) * [Recording a fraction of requests](#recording-a-fraction-of-requests) * [Limiting request/response data](#limiting-requestresponse-data) * [Clearing logged data](#clearing-logged-data) * [Contributing](#contributing) * [Development Environment](#development-environment) ## Requirements Silk has been tested with: * Django: 4.2, 5.1, 5.2, 6.0 * Python: 3.10, 3.11, 3.12, 3.13, 3.14 ## Installation Via pip into a `virtualenv`: ```bash pip install django-silk ``` To including optional formatting of python snippets: ```bash pip install django-silk[formatting] ``` In `settings.py` add the following: ```python MIDDLEWARE = [ ... 'silk.middleware.SilkyMiddleware', ... ] TEMPLATES = [{ ... 'OPTIONS': { 'context_processors': [ ... 'django.template.context_processors.request', ], }, }] INSTALLED_APPS = ( ... 'silk' ) ``` **Note:** The order of middleware is sensitive. If any middleware placed before `silk.middleware.SilkyMiddleware` returns a response without invoking its `get_response`, the `SilkyMiddleware` won’t run. To avoid this, ensure that middleware preceding `SilkyMiddleware` does not bypass or return a response without calling its `get_response`. For further details, check out the [Django documentation](https://docs.djangoproject.com/en/dev/topics/http/middleware/#middleware-order-and-layering). **Note:** If you are using `django.middleware.gzip.GZipMiddleware`, place that **before** `silk.middleware.SilkyMiddleware`, otherwise you will get an encoding error. If you want to use custom middleware, for example you developed the subclass of `silk.middleware.SilkyMiddleware`, so you can use this combination of settings: ```python # Specify the path where is the custom middleware placed SILKY_MIDDLEWARE_CLASS = 'path.to.your.middleware.MyCustomSilkyMiddleware' # Use this variable in list of middleware MIDDLEWARE = [ ... SILKY_MIDDLEWARE_CLASS, ... ] ``` To enable access to the user interface add the following to your `urls.py`: ```python urlpatterns += [path('silk/', include('silk.urls', namespace='silk'))] ``` before running migrate: ```bash python manage.py migrate python manage.py collectstatic ``` Silk will automatically begin interception of requests and you can proceed to add profiling if required. The UI can be reached at `/silk/` ### Alternative Installation Via [github tags](https://github.com/jazzband/django-silk/releases): ```bash pip install git+https://github.com/jazzband/django-silk.git@#egg=django_silk ``` You can install from master using the following, but please be aware that the version in master may not be working for all versions specified in [requirements](#requirements) ```bash pip install -e git+https://github.com/jazzband/django-silk.git#egg=django_silk ``` ## Features Silk primarily consists of: * Middleware for intercepting Requests/Responses * A wrapper around SQL execution for profiling of database queries * A context manager/decorator for profiling blocks of code and functions either manually or dynamically. * A user interface for inspection and visualisation of the above. ### Request Inspection The Silk middleware intercepts and stores requests and responses in the configured database. These requests can then be filtered and inspecting using Silk's UI through the request overview: It records things like: * Time taken * Num. queries * Time spent on queries * Request/Response headers * Request/Response bodies and so on. Further details on each request are also available by clicking the relevant request: ### SQL Inspection Silk also intercepts SQL queries that are generated by each request. We can get a summary on things like the tables involved, number of joins and execution time (the table can be sorted by clicking on a column header): Before diving into the stack trace to figure out where this request is coming from: ### Profiling Turn on the SILKY_PYTHON_PROFILER setting to use Python's built-in `cProfile` profiler. Each request will be separately profiled and the profiler's output will be available on the request's Profiling page in the Silk UI. Note that as of Python 3.12, `cProfile` cannot run concurrently so [django-silk under Python 3.12 and later will not profile if another profile is running](https://github.com/jazzband/django-silk/pull/692) (even its own profiler in another thread). ```python SILKY_PYTHON_PROFILER = True ``` If you would like to also generate a binary `.prof` file set the following: ```python SILKY_PYTHON_PROFILER_BINARY = True ``` When enabled, a graph visualisation generated using [gprof2dot](https://github.com/jrfonseca/gprof2dot) and [viz.js](https://github.com/almende/vis) is shown in the profile detail page: A custom storage class can be used for the saved generated binary `.prof` files: ```python # For Django >= 4.2 and Django-Silk >= 5.1.0: # See https://docs.djangoproject.com/en/5.0/ref/settings/#std-setting-STORAGES STORAGES = { 'SILKY_STORAGE': { 'BACKEND': 'path.to.StorageClass', }, # ... } # For Django < 4.2 or Django-Silk < 5.1.0 SILKY_STORAGE_CLASS = 'path.to.StorageClass' ``` The default storage class is `silk.storage.ProfilerResultStorage`, and when using that you can specify a path of your choosing. You must ensure the specified directory exists. ```python # If this is not set, MEDIA_ROOT will be used. SILKY_PYTHON_PROFILER_RESULT_PATH = '/path/to/profiles/' ``` A download button will become available with a binary `.prof` file for every request. This file can be used for further analysis using [snakeviz](https://github.com/jiffyclub/snakeviz) or other cProfile tools To retrieve which endpoint generates a specific profile file it is possible to add a stub of the request path in the file name with the following: ```python SILKY_PYTHON_PROFILER_EXTENDED_FILE_NAME = True ``` Silk can also be used to profile specific blocks of code/functions. It provides a decorator and a context manager for this purpose. For example: ```python from silk.profiling.profiler import silk_profile @silk_profile(name='View Blog Post') def post(request, post_id): p = Post.objects.get(pk=post_id) return render(request, 'post.html', { 'post': p }) ``` Whenever a blog post is viewed we get an entry within the Silk UI: Silk profiling not only provides execution time, but also collects SQL queries executed within the block in the same fashion as with requests: #### Decorator The silk decorator can be applied to both functions and methods ```python from silk.profiling.profiler import silk_profile # Profile a view function @silk_profile(name='View Blog Post') def post(request, post_id): p = Post.objects.get(pk=post_id) return render(request, 'post.html', { 'post': p }) # Profile a method in a view class class MyView(View): @silk_profile(name='View Blog Post') def get(self, request): p = Post.objects.get(pk=post_id) return render(request, 'post.html', { 'post': p }) ``` #### Context Manager Using a context manager means we can add additional context to the name which can be useful for narrowing down slowness to particular database records. ```python def post(request, post_id): with silk_profile(name='View Blog Post #%d' % self.pk): p = Post.objects.get(pk=post_id) return render(request, 'post.html', { 'post': p }) ``` #### Dynamic Profiling One of Silk's more interesting features is dynamic profiling. If for example we wanted to profile a function in a dependency to which we only have read-only access (e.g. system python libraries owned by root) we can add the following to `settings.py` to apply a decorator at runtime: ```python SILKY_DYNAMIC_PROFILING = [{ 'module': 'path.to.module', 'function': 'MyClass.bar' }] ``` which is roughly equivalent to: ```python class MyClass: @silk_profile() def bar(self): pass ``` The below summarizes the possibilities: ```python """ Dynamic function decorator """ SILKY_DYNAMIC_PROFILING = [{ 'module': 'path.to.module', 'function': 'foo' }] # ... is roughly equivalent to @silk_profile() def foo(): pass """ Dynamic method decorator """ SILKY_DYNAMIC_PROFILING = [{ 'module': 'path.to.module', 'function': 'MyClass.bar' }] # ... is roughly equivalent to class MyClass: @silk_profile() def bar(self): pass """ Dynamic code block profiling """ SILKY_DYNAMIC_PROFILING = [{ 'module': 'path.to.module', 'function': 'foo', # Line numbers are relative to the function as opposed to the file in which it resides 'start_line': 1, 'end_line': 2, 'name': 'Slow Foo' }] # ... is roughly equivalent to def foo(): with silk_profile(name='Slow Foo'): print (1) print (2) print(3) print(4) ``` Note that dynamic profiling behaves in a similar fashion to that of the python mock framework in that we modify the function in-place e.g: ```python """ my.module """ from another.module import foo # ...do some stuff foo() # ...do some other stuff ``` ,we would profile `foo` by dynamically decorating `my.module.foo` as opposed to `another.module.foo`: ```python SILKY_DYNAMIC_PROFILING = [{ 'module': 'my.module', 'function': 'foo' }] ``` If we were to apply the dynamic profile to the functions source module `another.module.foo` **after** it has already been imported, no profiling would be triggered. #### Custom Logic for Profiling Sometimes you may want to dynamically control when the profiler runs. You can write your own logic for when to enable the profiler. To do this add the following to your `settings.py`: This setting is mutually exclusive with SILKY_PYTHON_PROFILER and will be used over it if present. It will work with SILKY_DYNAMIC_PROFILING. ```python def my_custom_logic(request): return 'profile_requests' in request.session SILKY_PYTHON_PROFILER_FUNC = my_custom_logic # profile only session has recording enabled. ``` You can also use a `lambda`. ```python # profile only session has recording enabled. SILKY_PYTHON_PROFILER_FUNC = lambda request: 'profile_requests' in request.session ``` ### Code Generation Silk currently generates two bits of code per request: Both are intended for use in replaying the request. The curl command can be used to replay via command-line and the python code can be used within a Django unit test or simply as a standalone script. ## Configuration ### Authentication/Authorisation By default anybody can access the Silk user interface by heading to `/silk/`. To enable your Django auth backend place the following in `settings.py`: ```python SILKY_AUTHENTICATION = True # User must login SILKY_AUTHORISATION = True # User must have permissions ``` If `SILKY_AUTHORISATION` is `True`, by default Silk will only authorise users with `is_staff` attribute set to `True`. You can customise this using the following in `settings.py`: ```python def my_custom_perms(user): return user.is_allowed_to_use_silk SILKY_PERMISSIONS = my_custom_perms ``` You can also use a `lambda`. ```python SILKY_PERMISSIONS = lambda user: user.is_superuser ``` ### Request/Response bodies By default, Silk will save down the request and response bodies for each request for future viewing no matter how large. If Silk is used in production under heavy volume with large bodies this can have a huge impact on space/time performance. This behaviour can be configured with the following options: ```python SILKY_MAX_REQUEST_BODY_SIZE = -1 # Silk takes anything <0 as no limit SILKY_MAX_RESPONSE_BODY_SIZE = 1024 # If response body>1024 bytes, ignore ``` ### Meta-Profiling Sometimes it is useful to be able to see what effect Silk is having on the request/response time. To do this add the following to your `settings.py`: ```python SILKY_META = True ``` Silk will then record how long it takes to save everything down to the database at the end of each request: Note that in the above screenshot, this means that the request took 29ms (22ms from Django and 7ms from Silk) ### Recording a Fraction of Requests On high-load sites it may be helpful to only record a fraction of the requests that are made. To do this add the following to your `settings.py`: Note: This setting is mutually exclusive with SILKY_INTERCEPT_FUNC. ```python SILKY_INTERCEPT_PERCENT = 50 # log only 50% of requests ``` #### Custom Logic for Recording Requests On high-load sites it may also be helpful to write your own logic for when to intercept requests. To do this add the following to your `settings.py`: Note: This setting is mutually exclusive with SILKY_INTERCEPT_PERCENT. ```python def my_custom_logic(request): return 'record_requests' in request.session SILKY_INTERCEPT_FUNC = my_custom_logic # log only session has recording enabled. ``` You can also use a `lambda`. ```python # log only session has recording enabled. SILKY_INTERCEPT_FUNC = lambda request: 'record_requests' in request.session ``` ### Limiting request/response data To make sure silky garbage collects old request/response data, a config var can be set to limit the number of request/response rows it stores. ```python SILKY_MAX_RECORDED_REQUESTS = 10**4 ``` The garbage collection is only run on a percentage of requests to reduce overhead. It can be adjusted with this config: ```python SILKY_MAX_RECORDED_REQUESTS_CHECK_PERCENT = 10 ``` In case you want decouple silk's garbage collection from your webserver's request processing, set SILKY_MAX_RECORDED_REQUESTS_CHECK_PERCENT=0 and trigger it manually, e.g. in a cron job: ```bash python manage.py silk_request_garbage_collect ``` ### Enable query analysis To enable query analysis when supported by the dbms a config var can be set in order to execute queries with the analyze features. ```python SILKY_ANALYZE_QUERIES = True ``` **Warning:** This setting may cause the database to execute the same query twice, depending on the backend. For instance, `EXPLAIN ANALYZE` in Postgres will [actually execute the query](https://www.postgresql.org/docs/current/sql-explain.html), which may result in unexpected data updates. Set this to True with caution. To pass additional params for profiling when supported by the dbms (e.g. VERBOSE, FORMAT JSON), you can do this in the following manner. ```python SILKY_EXPLAIN_FLAGS = {'format':'JSON', 'costs': True} ``` ### Masking sensitive data on request body By default, Silk is filtering values that contains the following keys (they are case insensitive) ```python SILKY_SENSITIVE_KEYS = {'username', 'api', 'token', 'key', 'secret', 'password', 'signature'} ``` But sometimes, you might want to have your own sensitive keywords, then above configuration can be modified ```python SILKY_SENSITIVE_KEYS = {'custom-password'} ``` ### Clearing logged data A management command will wipe out all logged data: ```bash python manage.py silk_clear_request_log ``` ## Contributing [![Jazzband](https://jazzband.co/static/img/jazzband.svg)](https://jazzband.co/) This is a [Jazzband](https://jazzband.co/) project. By contributing you agree to abide by the [Contributor Code of Conduct](https://jazzband.co/about/conduct) and follow the [guidelines](https://jazzband.co/about/guidelines). ### Development Environment Silk features a project named `project` that can be used for `silk` development. It has the `silk` code symlinked so you can work on the sample `project` and on the `silk` package at the same time. In order to setup local development you should first install all the dependencies for the test `project`. From the root of the `project` directory: ```bash pip install -r requirements.txt ``` You will also need to install `silk`'s dependencies. From the root of the git repository: ```bash pip install -e . ``` At this point your virtual environment should have everything it needs to run both the sample `project` and `silk` successfully. Before running, you must set the `DB_ENGINE` and `DB_NAME` environment variables: ```bash export DB_ENGINE=sqlite3 export DB_NAME=db.sqlite3 ``` For other combinations, check [`tox.ini`](./tox.ini). Now from the root of the sample `project` apply the migrations ```bash python manage.py migrate ``` Now from the root of the sample `project` directory start the django server ```bash python manage.py runserver ``` #### Running the tests ```bash cd project python manage.py test ``` Happy profiling! ================================================ FILE: docs/Makefile ================================================ # Makefile for Sphinx documentation # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/silk.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/silk.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/silk" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/silk" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." ================================================ FILE: docs/conf.py ================================================ # # silk documentation build configuration file, created by # sphinx-quickstart on Sun Jun 22 13:51:12 2014. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import pkg_resources # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. # The master toctree document. master_doc = 'index' # General information about the project. project = 'silk' copyright = '2014, Michael Ford' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The full version, including alpha/beta/rc tags. release = pkg_resources.get_distribution("django-silk").version # The short X.Y version. version = '.'.join(release.split('.')[:2]) # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all # documents. # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. # keep_warnings = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'default' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". # html_title = None # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. # html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. # html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # html_additional_pages = {} # If false, no module index is generated. # html_domain_indices = True # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'silkdoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # 'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ ('index', 'silk.tex', 'silk Documentation', 'Michael Ford', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # If true, show page references after internal links. # latex_show_pagerefs = False # If true, show URL addresses after external links. # latex_show_urls = False # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'silk', 'silk Documentation', ['Michael Ford'], 1), ('profiling', 'Profiling', 'Profiling', ['Michael Ford'], 2), ] # If true, show URL addresses after external links. # man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'silk', 'silk Documentation', 'Michael Ford', 'silk', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. # texinfo_appendices = [] # If false, no module index is generated. # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. # texinfo_no_detailmenu = False ================================================ FILE: docs/configuration.rst ================================================ Configuration ============= Authentication and Authorisation -------------------------------- By default anybody can access the Silk user interface by heading to `/silk/`. To enable your Django auth backend place the following in `settings.py`: .. code-block:: python SILKY_AUTHENTICATION = True # User must login SILKY_AUTHORISATION = True # User must have permissions If ``SILKY_AUTHORISATION`` is ``True``, by default Silk will only authorise users with ``is_staff`` attribute set to ``True``. You can customise this using the following in ``settings.py``: .. code-block:: python def my_custom_perms(user): return user.is_allowed_to_use_silk SILKY_PERMISSIONS = my_custom_perms Request and Response bodies --------------------------- By default, Silk will save down the request and response bodies for each request for future viewing no matter how large. If Silk is used in production under heavy volume with large bodies this can have a huge impact on space/time performance. This behaviour can be configured with following options: .. code-block:: python SILKY_MAX_REQUEST_BODY_SIZE = -1 # Silk takes anything <0 as no limit SILKY_MAX_RESPONSE_BODY_SIZE = 1024 # If response body>1024kb, ignore Meta-Profiling -------------- Sometimes its useful to be able to see what effect Silk is having on the request/response time. To do this add the following to your `settings.py`: .. code-block:: python SILKY_META = True Silk will then record how long it takes to save everything down to the database at the end of each request: .. image:: /images/meta.png Note that in the above screenshot, this means that the request took 29ms (22ms from Django and 7ms from Silk) Limiting request and response data ---------------------------------- To make sure silky garbage collects old request/response data, a config var can be set to limit the number of request/response rows it stores. .. code-block:: python SILKY_MAX_RECORDED_REQUESTS = 10**4 The garbage collection is only run on a percentage of requests to reduce overhead. It can be adjusted with this config: .. code-block:: python SILKY_MAX_RECORDED_REQUESTS_CHECK_PERCENT = 10 ================================================ FILE: docs/index.rst ================================================ .. silk documentation master file, created by sphinx-quickstart on Sun Jun 22 13:51:12 2014. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. Silk ================================ .. toctree:: :maxdepth: 2 quickstart profiling configuration troubleshooting Silk is a live profiling and inspection tool for the Django framework. Silk intercepts and stores HTTP requests and database queries before presenting them in a user interface for further inspection: .. image:: /images/1.png A **live demo** is available `here`_. .. _here: http://mtford.co.uk/silk/ Features -------- - Inspect HTTP requests and responses - Query parameters - Headers - Bodies - Execution Time - Database Queries - Number - Time taken - SQL query inspection - Profiling of arbitrary code blocks via a Python context manager and decorator - Execution Time - Database Queries - Can also be injected dynamically at runtime e.g. if read-only dependency. - Authentication/Authorisation for production use Requirements ------------ * Django: 4.2, 5.1, 5.2, 6.0 * Python: 3.10, 3.11, 3.12, 3.13, 3.14 ================================================ FILE: docs/profiling.rst ================================================ Profiling ========= Silk can be used to profile arbitrary blocks of code and provides ``silk_profile``, a Python decorator and a context manager for this purpose. Profiles will then appear in the 'Profiling' tab within Silk's user interface. Decorator --------- The decorator can be applied to both functions and methods: .. code-block:: python @silk_profile(name='View Blog Post') def post(request, post_id): p = Post.objects.get(pk=post_id) return render(request, 'post.html', { 'post': p }) .. code-block:: python class MyView(View): @silk_profile(name='View Blog Post') def get(self, request): p = Post.objects.get(pk=post_id) return render(request, 'post.html', { 'post': p }) Context Manager --------------- ``silk_profile`` can also be used as a context manager: .. code-block:: python def post(request, post_id): with silk_profile(name='View Blog Post #%d' % self.pk): p = Post.objects.get(pk=post_id) return render(request, 'post.html', { 'post': p }) Dynamic Profiling ----------------- Decorators and context managers can also be injected at run-time. This is useful if we want to narrow down slow requests/database queries to dependencies. Dynamic profiling is configured via the ``SILKY_DYNAMIC_PROFILING`` option in your ``settings.py``: .. code-block:: python """ Dynamic function decorator """ SILKY_DYNAMIC_PROFILING = [{ 'module': 'path.to.module', 'function': 'foo' }] # ... is roughly equivalent to @silk_profile() def foo(): pass """ Dynamic method decorator """ SILKY_DYNAMIC_PROFILING = [{ 'module': 'path.to.module', 'function': 'MyClass.bar' }] # ... is roughly equivalent to class MyClass: @silk_profile() def bar(self): pass """ Dynamic code block profiling """ SILKY_DYNAMIC_PROFILING = [{ 'module': 'path.to.module', 'function': 'foo', # Line numbers are relative to the function as opposed to the file in which it resides 'start_line': 1, 'end_line': 2, 'name': 'Slow Foo' }] # ... is roughly equivalent to def foo(): with silk_profile(name='Slow Foo'): print (1) print (2) print(3) print(4) Note that dynamic profiling behaves in a similar fashion to that of the python mock framework in that we modify the function in-place e.g: .. code-block:: python """ my.module """ from another.module import foo # ...do some stuff foo() # ...do some other stuff We would profile ``foo`` by dynamically decorating `my.module.foo` as opposed to ``another.module.foo``: .. code-block:: python SILKY_DYNAMIC_PROFILING = [{ 'module': 'my.module', 'function': 'foo' }] If we were to apply the dynamic profile to the functions source module ``another.module.foo`` *after* it has already been imported, no profiling would be triggered. ================================================ FILE: docs/quickstart.rst ================================================ Quick Start =========== Silk is installed like any other Django app. First install via pip: .. code-block:: bash pip install django-silk Add the following to your ``settings.py``: .. code-block:: python MIDDLEWARE = [ ... 'silk.middleware.SilkyMiddleware', ... ] TEMPLATES = [{ ... 'OPTIONS': { 'context_processors': [ ... 'django.template.context_processors.request', ], }, }] INSTALLED_APPS = [ ... 'silk.apps.SilkAppConfig' ] Add the following to your ``urls.py``: .. code-block:: python urlpatterns += [path('silk', include('silk.urls', namespace='silk'))] Run ``migrate`` to create Silk's database tables: .. code-block:: bash python manage.py migrate And voila! Silk will begin intercepting requests and queries which you can inspect by visiting ``/silk/`` Python Snippet Formatting ------------------------- Silk supports generating Python snippets to reproduce requests. To enable autopep8 formatting of these snippets, install Silk with the `formatting` extras: .. code-block:: bash pip install django-silk[formatting] Other Installation Options -------------------------- You can download a release from `github `_ and then install using pip: .. code-block:: bash pip install django-silk-.tar.gz You can also install directly from the github repo but please note that this version is not guaranteed to be working: .. code-block:: bash pip install -e git+https://github.com/jazzband/django-silk.git#egg=django_silk ================================================ FILE: docs/troubleshooting.rst ================================================ Troubleshooting =============== The below details common problems when using Silk, most of which have been derived from the solutions to github issues. Unicode ------- Silk saves down the request and response bodies of each HTTP request by default. These bodies are often UTF encoded and hence it is important that Silk's database tables are also UTF encoded. Django has no facility for enforcing this and instead assumes that the configured database defaults to UTF. If you see errors like: Incorrect string value: '\xCE\xBB, \xCF\x86...' for column 'raw_body' at row... Then it's likely your database is not configured correctly for UTF encoding. See this `github issue `_ for more details and workarounds. Context Processor ----------------- Silk requires the template context to include a ``request`` object in order to save and analyze it. If you see errors like: .. code-block:: text File "/service/venv/lib/python3.12/site-packages/silk/templatetags/silk_nav.py", line 9, in navactive path = request.path ^^^^^^^^^^^^ AttributeError: 'str' object has no attribute 'path' Include ``django.template.context_processors.request`` in your Django settings' ``TEMPLATES`` context processors as `recommended `_. Middleware ---------- The order of middleware is sensitive. If any middleware placed before ``silk.middleware.SilkyMiddleware`` returns a response without invoking its ``get_response``, the ``SilkyMiddleware`` won’t run. To avoid this, ensure that middleware preceding ``SilkyMiddleware`` does not bypass or return a response without calling its ``get_response``. For further details, check out the `Django documentation `. Garbage Collection ------------------ To `avoid `_ `deadlock `_ `issues `_, you might want to decouple silk's garbage collection from your webserver's request processing, set ``SILKY_MAX_RECORDED_REQUESTS_CHECK_PERCENT=0`` and trigger it manually, e.g. in a cron job: .. code-block:: bash python manage.py silk_request_garbage_collect ================================================ FILE: gulpfile.js ================================================ let gulp = require('gulp'), sass = require('gulp-sass'); gulp.task('watch', function () { gulp.watch('scss/**/*.scss', gulp.series('sass')); }); gulp.task('sass', function () { return gulp.src('scss/**/*.scss') .pipe(sass().on('error', sass.logError)) .pipe(gulp.dest('silk/static/silk/css')); }); ================================================ FILE: package.json ================================================ { "name": "silk", "version": "5.5.0", "description": "https://github.com/jazzband/django-silk", "main": "index.js", "directories": { "doc": "docs", "test": "tests" }, "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "repository": { "type": "git", "url": "https://github.com/jazzband/django-silk.git" }, "author": "", "license": "ISC", "bugs": { "url": "https://github.com/jazzband/django-silk/issues" }, "homepage": "https://github.com/jazzband/django-silk", "devDependencies": { "gulp": "^4.0.2", "gulp-sass": "^4.0.2" }, "dependencies": {} } ================================================ FILE: project/example_app/__init__.py ================================================ ================================================ FILE: project/example_app/admin.py ================================================ from django.contrib import admin from django.urls import reverse from .models import Blind @admin.register(Blind) class BlindAdmin(admin.ModelAdmin): list_display = ('desc', 'thumbnail', 'name', 'child_safe') list_editable = ('name', 'child_safe') @admin.display( description='Photo' ) def thumbnail(self, obj): try: img_tag = '' % obj.photo.url except ValueError: return '' url = self._blind_url(obj) return f'{img_tag}' def _blind_url(self, obj): url = reverse('admin:example_app_blind_change', args=(obj.id, )) return url @admin.display( description='Blind' ) def desc(self, obj): desc = str(obj) url = self._blind_url(obj) return f'{desc}' ================================================ FILE: project/example_app/migrations/0001_initial.py ================================================ # Generated by Django 1.9.7 on 2016-07-08 13:19 from django.db import migrations, models class Migration(migrations.Migration): initial = True dependencies = [ ] operations = [ migrations.CreateModel( name='Blind', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('photo', models.ImageField(upload_to=b'products')), ('name', models.TextField()), ('child_safe', models.BooleanField(default=False)), ], options={ 'abstract': False, }, ), ] ================================================ FILE: project/example_app/migrations/0002_alter_blind_photo.py ================================================ # Generated by Django 3.2 on 2021-04-12 22:45 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('example_app', '0001_initial'), ] operations = [ migrations.AlterField( model_name='blind', name='photo', field=models.ImageField(upload_to='products'), ), ] ================================================ FILE: project/example_app/migrations/0003_blind_unique_name_if_provided.py ================================================ # Generated by Django 3.2.16 on 2022-10-28 08:08 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('example_app', '0002_alter_blind_photo'), ] operations = [ migrations.AddConstraint( model_name='blind', constraint=models.UniqueConstraint(condition=models.Q(('name', ''), _negated=True), fields=('name',), name='unique_name_if_provided'), ), ] ================================================ FILE: project/example_app/migrations/__init__.py ================================================ ================================================ FILE: project/example_app/models.py ================================================ from django.db import models # Create your models here. from django.db.models import BooleanField, ImageField, TextField class Product(models.Model): photo = ImageField(upload_to='products') class Meta: abstract = True class Blind(Product): name = TextField() child_safe = BooleanField(default=False) def __str__(self): return self.name class Meta: constraints = [ models.UniqueConstraint( fields=["name"], condition=~models.Q(name=""), name="unique_name_if_provided", ), ] ================================================ FILE: project/example_app/templates/example_app/blind_form.html ================================================

Example App

Use this app for testing and playing around with Silk. Displays a Blind creation form.

{% csrf_token %} {{ form.as_p }}
================================================ FILE: project/example_app/templates/example_app/index.html ================================================

Example App

Use this app for testing and playing around with Silk. Displays a range of Blinds. Use admin to add them.

{% for blind in blinds %} {% endfor %}
Photo Name Child safe?
{% if blind.photo %}{% endif %} {{ blind.name }} {% if blind.child_safe %}Yes{% else %}No{% endif %}
================================================ FILE: project/example_app/templates/example_app/login.html ================================================ {% if form.errors %}

Your username and password didn't match. Please try again.

{% endif %}
{% csrf_token %}
{{ form.username.label_tag }} {{ form.username }}
{{ form.password.label_tag }} {{ form.password }}
================================================ FILE: project/example_app/tests.py ================================================ # Create your tests here. ================================================ FILE: project/example_app/urls.py ================================================ from django.urls import path from . import views app_name = 'example_app' urlpatterns = [ path(route='', view=views.index, name='index'), path(route='create', view=views.ExampleCreateView.as_view(), name='create'), ] ================================================ FILE: project/example_app/views.py ================================================ from time import sleep # Create your views here. from django.shortcuts import render from django.urls import reverse_lazy from django.views.generic import CreateView from example_app import models from silk.profiling.profiler import silk_profile def index(request): @silk_profile() def do_something_long(): sleep(1.345) with silk_profile(name='Why do this take so long?'): do_something_long() return render(request, 'example_app/index.html', {'blinds': models.Blind.objects.all()}) class ExampleCreateView(CreateView): model = models.Blind fields = ['name'] success_url = reverse_lazy('example_app:index') ================================================ FILE: project/manage.py ================================================ #!/usr/bin/env python """Define the Django Silk management entry.""" import os import sys if __name__ == "__main__": os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings") from django.core.management import execute_from_command_line execute_from_command_line(sys.argv) ================================================ FILE: project/project/__init__.py ================================================ __author__ = 'mtford' ================================================ FILE: project/project/settings.py ================================================ import os BASE_DIR = os.path.dirname(os.path.dirname(__file__)) SECRET_KEY = 'ey5!m&h-uj6c7dzp@(o1%96okkq4!&bjja%oi*v3r=2t(!$7os' DEBUG = True DEBUG_PROPAGATE_EXCEPTIONS = True ALLOWED_HOSTS = [] INSTALLED_APPS = ( 'django.contrib.staticfiles', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.messages', 'django.contrib.sessions', 'silk', 'example_app' ) ROOT_URLCONF = 'project.urls' DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' MIDDLEWARE = [ 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'silk.middleware.SilkyMiddleware' ] WSGI_APPLICATION = 'wsgi.application' DB_ENGINE = os.environ.get("DB_ENGINE", "postgresql") DATABASES = { "default": { "ENGINE": f"django.db.backends.{DB_ENGINE}", "NAME": os.environ.get("DB_NAME", "postgres"), "USER": os.environ.get("DB_USER", 'postgres'), "PASSWORD": os.environ.get("DB_PASSWORD", "postgres"), "HOST": os.environ.get("DB_HOST", "127.0.0.1"), "PORT": os.environ.get("DB_PORT", 5432), "ATOMIC_REQUESTS": True }, } LANGUAGE_CODE = 'en-us' TIME_ZONE = 'UTC' USE_I18N = True USE_TZ = True LOGGING = { 'version': 1, 'formatters': { 'mosayc': { 'format': '%(asctime)-15s %(levelname)-7s %(message)s [%(funcName)s (%(filename)s:%(lineno)s)]', } }, 'handlers': { 'console': { 'level': 'DEBUG', 'class': 'logging.StreamHandler', 'formatter': 'mosayc' } }, 'loggers': { 'silk': { 'handlers': ['console'], 'level': 'DEBUG' } }, } STATIC_URL = '/static/' STATICFILES_FINDERS = ( 'django.contrib.staticfiles.finders.FileSystemFinder', 'django.contrib.staticfiles.finders.AppDirectoriesFinder', ) TEMP_DIR = os.path.join(BASE_DIR, "tmp") STATIC_ROOT = os.path.join(TEMP_DIR, "static") if not os.path.exists(STATIC_ROOT): os.makedirs(STATIC_ROOT) MEDIA_ROOT = BASE_DIR + '/media/' MEDIA_URL = '/media/' if not os.path.exists(MEDIA_ROOT): os.mkdir(MEDIA_ROOT) TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.request', ], }, }, ] LOGIN_URL = '/login/' LOGIN_REDIRECT_URL = '/' SILKY_META = True SILKY_PYTHON_PROFILER = True SILKY_PYTHON_PROFILER_BINARY = True # Do not garbage collect for tests SILKY_MAX_RECORDED_REQUESTS_CHECK_PERCENT = 0 # SILKY_AUTHENTICATION = True # SILKY_AUTHORISATION = True ================================================ FILE: project/project/urls.py ================================================ from django.conf import settings from django.conf.urls.static import static from django.contrib import admin from django.contrib.auth import views from django.urls import include, path urlpatterns = [ path( route='silk/', view=include('silk.urls', namespace='silk'), ), path( route='example_app/', view=include('example_app.urls', namespace='example_app'), ), path(route='admin/', view=admin.site.urls), ] urlpatterns += [ path( route='login/', view=views.LoginView.as_view( template_name='example_app/login.html' ), name='login', ), ] urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) + \ static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) ================================================ FILE: project/tests/__init__.py ================================================ from . import * # noqa: F401, F403 ================================================ FILE: project/tests/data/__init__.py ================================================ __author__ = 'mtford' ================================================ FILE: project/tests/data/dynamic.py ================================================ def foo(): print('1') print('2') print('3') def foo2(): print('1') print('2') print('3') ================================================ FILE: project/tests/factories.py ================================================ import factory import factory.fuzzy from example_app.models import Blind from silk.models import Request, Response, SQLQuery HTTP_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'HEAD', 'OPTIONS'] STATUS_CODES = [200, 201, 300, 301, 302, 401, 403, 404] class SQLQueryFactory(factory.django.DjangoModelFactory): query = factory.Sequence(lambda num: 'SELECT foo FROM bar WHERE foo=%s' % num) traceback = factory.Sequence(lambda num: 'Traceback #%s' % num) class Meta: model = SQLQuery class RequestMinFactory(factory.django.DjangoModelFactory): path = factory.Faker('uri_path') method = factory.fuzzy.FuzzyChoice(HTTP_METHODS) class Meta: model = Request class ResponseFactory(factory.django.DjangoModelFactory): request = factory.SubFactory(RequestMinFactory) status_code = factory.fuzzy.FuzzyChoice(STATUS_CODES) class Meta: model = Response class BlindFactory(factory.django.DjangoModelFactory): name = factory.Faker('pystr', min_chars=5, max_chars=10) child_safe = factory.Faker('pybool') photo = factory.django.ImageField() class Meta: model = Blind ================================================ FILE: project/tests/test_app_config.py ================================================ from django.apps import apps as proj_apps from django.test import TestCase from silk.apps import SilkAppConfig class TestAppConfig(TestCase): """ Test if correct AppConfig class is loaded by Django. """ def test_app_config_loaded(self): silk_app_config = proj_apps.get_app_config("silk") self.assertIsInstance(silk_app_config, SilkAppConfig) ================================================ FILE: project/tests/test_code.py ================================================ from collections import namedtuple from django.test import TestCase from silk.views.code import _code, _code_context, _code_context_from_request FILE_PATH = __file__ LINE_NUM = 5 END_LINE_NUM = 10 with open(__file__) as f: ACTUAL_LINES = [line + '\n' for line in f.read().split('\n')] class CodeTestCase(TestCase): def assertActualLineEqual(self, actual_line, end_line_num=None): expected_actual_line = ACTUAL_LINES[LINE_NUM - 1:end_line_num or LINE_NUM] self.assertEqual(actual_line, expected_actual_line) def assertCodeEqual(self, code): expected_code = [line.strip('\n') for line in ACTUAL_LINES[0:LINE_NUM + 10]] + [''] self.assertEqual(code, expected_code) def test_code(self): for end_line_num in None, END_LINE_NUM: actual_line, code = _code(FILE_PATH, LINE_NUM, end_line_num) self.assertActualLineEqual(actual_line, end_line_num) self.assertCodeEqual(code) def test_code_context(self): for end_line_num in None, END_LINE_NUM: for prefix in '', 'salchicha_': context = _code_context(FILE_PATH, LINE_NUM, end_line_num, prefix) self.assertActualLineEqual(context[prefix + 'actual_line'], end_line_num) self.assertCodeEqual(context[prefix + 'code']) self.assertEqual(context[prefix + 'file_path'], FILE_PATH) self.assertEqual(context[prefix + 'line_num'], LINE_NUM) def test_code_context_from_request(self): for end_line_num in None, END_LINE_NUM: for prefix in '', 'salchicha_': request = namedtuple('Request', 'GET')(dict(file_path=FILE_PATH, line_num=LINE_NUM)) context = _code_context_from_request(request, end_line_num, prefix) self.assertActualLineEqual(context[prefix + 'actual_line'], end_line_num) self.assertCodeEqual(context[prefix + 'code']) self.assertEqual(context[prefix + 'file_path'], FILE_PATH) self.assertEqual(context[prefix + 'line_num'], LINE_NUM) ================================================ FILE: project/tests/test_code_gen_curl.py ================================================ import shlex from unittest import TestCase from silk.code_generation.curl import curl_cmd class TestCodeGenCurl(TestCase): def test_post_json(self): result = curl_cmd( url="https://example.org/alpha/beta", method="POST", body={"gamma": "delta"}, content_type="application/json", ) result_words = shlex.split(result) self.assertEqual(result_words, [ 'curl', '-X', 'POST', '-H', 'content-type: application/json', '-d', '{"gamma": "delta"}', 'https://example.org/alpha/beta' ]) ================================================ FILE: project/tests/test_code_gen_django.py ================================================ import textwrap from unittest import TestCase from silk.code_generation.django_test_client import gen class TestCodeGenDjango(TestCase): def test_post(self): result = gen( path="/alpha/beta", method="POST", data={"gamma": "delta", "epsilon": "zeta"}, content_type="application/x-www-form-urlencoded", ) self.assertEqual(result, textwrap.dedent("""\ from django.test import Client c = Client() response = c.post(path='/alpha/beta', data={'gamma': 'delta', 'epsilon': 'zeta'}, content_type='application/x-www-form-urlencoded') """)) ================================================ FILE: project/tests/test_collector.py ================================================ import cProfile import os.path import sys from django.test import TestCase from tests.util import DictStorage from silk.collector import DataCollector from silk.config import SilkyConfig from .factories import RequestMinFactory class TestCollector(TestCase): def test_singleton(self): a = DataCollector() b = DataCollector() c = DataCollector() self.assertTrue(a == b == c) def test_query_registration(self): mock_query = {} DataCollector().register_query(mock_query) self.assertIn(mock_query, list(DataCollector().queries.values())) def test_clear(self): self.test_query_registration() DataCollector().clear() self.assertFalse(DataCollector().queries) def test_finalise(self): request = RequestMinFactory() DataCollector().configure(request) with self.subTest("Default file-based storage"): DataCollector().finalise() file = DataCollector().request.prof_file self.assertIsNotNone(file) with file.storage.open(file.name) as f: content = f.read() self.assertTrue(content) # Some storages, such as S3Boto3Storage, don't support local file system path. # Simulate this behaviour using DictStorage. with self.subTest("Pathless storage"): request.prof_file.storage = DictStorage() DataCollector().finalise() file = DataCollector().request.prof_file self.assertIsNotNone(file) with file.storage.open(file.name) as f: content = f.read() self.assertTrue(content) self.assertGreater(len(content), 0) def test_configure_exception(self): other_profiler = cProfile.Profile() other_profiler.enable() collector = DataCollector() collector.configure() other_profiler.disable() if sys.version_info >= (3, 12): self.assertEqual(collector.local.pythonprofiler, None) else: self.assertIsNotNone(collector.local.pythonprofiler) collector.stop_python_profiler() def test_profile_file_name_with_disabled_extended_file_name(self): SilkyConfig().SILKY_PYTHON_PROFILER_EXTENDED_FILE_NAME = False request_path = 'normal/uri/' resulting_prefix = self._get_prof_file_name(request_path) self.assertEqual(resulting_prefix, '') def test_profile_file_name_with_enabled_extended_file_name(self): SilkyConfig().SILKY_PYTHON_PROFILER_EXTENDED_FILE_NAME = True request_path = 'normal/uri/' resulting_prefix = self._get_prof_file_name(request_path) self.assertEqual(resulting_prefix, 'normal_uri_') def test_profile_file_name_with_path_traversal_and_special_char(self): SilkyConfig().SILKY_PYTHON_PROFILER_EXTENDED_FILE_NAME = True request_path = 'spÉciàl/.././大/uri/@É/' resulting_prefix = self._get_prof_file_name(request_path) self.assertEqual(resulting_prefix, 'special_uri_e_') def test_profile_file_name_with_long_path(self): SilkyConfig().SILKY_PYTHON_PROFILER_EXTENDED_FILE_NAME = True request_path = 'long/path/' + 'a' * 100 resulting_prefix = self._get_prof_file_name(request_path) # the path is limited to 50 char plus the last `_` self.assertEqual(len(resulting_prefix), 51) @classmethod def _get_prof_file_name(cls, request_path: str) -> str: request = RequestMinFactory() request.path = request_path DataCollector().configure(request) DataCollector().finalise() file_path = DataCollector().request.prof_file.name filename = os.path.basename(file_path) return filename.replace(f"{request.id}.prof", "") ================================================ FILE: project/tests/test_command_garbage_collect.py ================================================ from django.core import management from django.test import TestCase from silk import models from silk.config import SilkyConfig from .factories import RequestMinFactory class TestViewClearDB(TestCase): def test_garbage_collect_command(self): SilkyConfig().SILKY_MAX_RECORDED_REQUESTS = 2 RequestMinFactory.create_batch(3) self.assertEqual(models.Request.objects.count(), 3) management.call_command("silk_request_garbage_collect") self.assertEqual(models.Request.objects.count(), 2) management.call_command("silk_request_garbage_collect", max_requests=1) self.assertEqual(models.Request.objects.count(), 1) management.call_command( "silk_request_garbage_collect", max_requests=0, verbosity=2 ) self.assertEqual(models.Request.objects.count(), 0) ================================================ FILE: project/tests/test_compat.py ================================================ import json from unittest.mock import Mock from django.test import TestCase from silk.model_factory import ResponseModelFactory DJANGO_META_CONTENT_TYPE = 'CONTENT_TYPE' HTTP_CONTENT_TYPE = 'content-type' class TestByteStringCompatForResponse(TestCase): def test_bytes_compat(self): """ Test ResponseModelFactory formats json with bytes content """ mock = Mock() mock.headers = {HTTP_CONTENT_TYPE: 'application/json;'} d = {'k': 'v'} mock.content = bytes(json.dumps(d), 'utf-8') mock.get = mock.headers.get factory = ResponseModelFactory(mock) body, content = factory.body() self.assertDictEqual(json.loads(body), d) ================================================ FILE: project/tests/test_config_auth.py ================================================ from django.contrib.auth.models import User from django.test import TestCase from django.urls import NoReverseMatch, reverse from silk.config import SilkyConfig, default_permissions from silk.middleware import silky_reverse class TestAuth(TestCase): def test_authentication(self): SilkyConfig().SILKY_AUTHENTICATION = True response = self.client.get(silky_reverse('requests')) self.assertEqual(response.status_code, 302) url = response.url try: # If we run tests within the django_silk project, a login url is available from example_app self.assertIn(reverse('login'), url) except NoReverseMatch: # Otherwise the Django default login url is used, in which case we can test for that instead self.assertIn('http://testserver/login/', url) def test_default_authorisation(self): SilkyConfig().SILKY_AUTHENTICATION = True SilkyConfig().SILKY_AUTHORISATION = True SilkyConfig().SILKY_PERMISSIONS = default_permissions username_and_password = 'bob' # bob is an imbecile and uses the same pass as his username user = User.objects.create(username=username_and_password) user.set_password(username_and_password) user.save() self.client.login(username=username_and_password, password=username_and_password) response = self.client.get(silky_reverse('requests')) self.assertEqual(response.status_code, 403) user.is_staff = True user.save() response = self.client.get(silky_reverse('requests')) self.assertEqual(response.status_code, 200) def test_custom_authorisation(self): SilkyConfig().SILKY_AUTHENTICATION = True SilkyConfig().SILKY_AUTHORISATION = True def custom_authorisation(user): return user.username.startswith('mike') SilkyConfig().SILKY_PERMISSIONS = custom_authorisation username_and_password = 'bob' # bob is an imbecile and uses the same pass as his username user = User.objects.create(username=username_and_password) user.set_password(username_and_password) user.save() self.client.login(username=username_and_password, password=username_and_password) response = self.client.get(silky_reverse('requests')) self.assertEqual(response.status_code, 403) user.username = 'mike2' user.save() response = self.client.get(silky_reverse('requests')) self.assertEqual(response.status_code, 200) ================================================ FILE: project/tests/test_config_long_urls.py ================================================ from unittest.mock import Mock from django.test import TestCase from silk.model_factory import RequestModelFactory class TestLongRequestUrl(TestCase): def test_no_long_url(self): url = '1234567890' * 19 # 190-character URL mock_request = Mock() mock_request.headers = {'content-type': 'text/plain'} mock_request.GET = {} mock_request.path = url mock_request.method = 'get' request_model = RequestModelFactory(mock_request).construct_request_model() self.assertEqual(request_model.path, url) def test_long_url(self): url = '1234567890' * 200 # 2000-character URL mock_request = Mock() mock_request.headers = {'content-type': 'text/plain'} mock_request.GET = {} mock_request.method = 'get' mock_request.path = url request_model = RequestModelFactory(mock_request).construct_request_model() self.assertEqual(request_model.path, f'{url[:94]}...{url[1907:]}') self.assertEqual(len(request_model.path), 190) ================================================ FILE: project/tests/test_config_max_body_size.py ================================================ from unittest.mock import Mock from django.test import TestCase from django.urls import reverse from silk.collector import DataCollector from silk.config import SilkyConfig from silk.model_factory import RequestModelFactory, ResponseModelFactory from silk.models import Request class TestMaxBodySizeRequest(TestCase): def test_no_max_request(self): SilkyConfig().SILKY_MAX_REQUEST_BODY_SIZE = -1 mock_request = Mock() mock_request.headers = {'content-type': 'text/plain'} mock_request.GET = {} mock_request.path = reverse('silk:requests') mock_request.method = 'get' mock_request.body = b'a' * 1000 # 1000 bytes? request_model = RequestModelFactory(mock_request).construct_request_model() self.assertTrue(request_model.raw_body) def test_max_request(self): SilkyConfig().SILKY_MAX_REQUEST_BODY_SIZE = 10 # 10kb mock_request = Mock() mock_request.headers = {'content-type': 'text/plain'} mock_request.GET = {} mock_request.method = 'get' mock_request.body = b'a' * 1024 * 100 # 100kb mock_request.path = reverse('silk:requests') request_model = RequestModelFactory(mock_request).construct_request_model() self.assertFalse(request_model.raw_body) class TestMaxBodySizeResponse(TestCase): def setUp(self): DataCollector().request = Request.objects.create() def test_no_max_response(self): SilkyConfig().SILKY_MAX_RESPONSE_BODY_SIZE = -1 mock_response = Mock() mock_response.headers = {'content-type': 'text/plain'} mock_response.content = b'a' * 1000 # 1000 bytes? mock_response.status_code = 200 mock_response.get = mock_response.headers.get response_model = ResponseModelFactory(mock_response).construct_response_model() self.assertTrue(response_model.raw_body) def test_max_response(self): SilkyConfig().SILKY_MAX_RESPONSE_BODY_SIZE = 10 # 10kb mock_response = Mock() mock_response.headers = {'content-type': 'text/plain'} mock_response.content = b'a' * 1024 * 100 # 100kb mock_response.status_code = 200 mock_response.get = mock_response.headers.get response_model = ResponseModelFactory(mock_response).construct_response_model() self.assertFalse(response_model.raw_body) ================================================ FILE: project/tests/test_config_meta.py ================================================ from unittest.mock import NonCallableMock from django.test import TestCase from silk.collector import DataCollector from silk.config import SilkyConfig from silk.middleware import SilkyMiddleware from silk.models import Request from .util import delete_all_models def fake_get_response(): def fake_response(): return 'hello world' return fake_response class TestConfigMeta(TestCase): def _mock_response(self): response = NonCallableMock() response.headers = {} response.status_code = 200 response.queries = [] response.get = response.headers.get response.content = '' return response def _execute_request(self): delete_all_models(Request) DataCollector().configure(Request.objects.create()) response = self._mock_response() SilkyMiddleware(fake_get_response)._process_response('', response) self.assertTrue(response.status_code == 200) objs = Request.objects.all() self.assertEqual(objs.count(), 1) r = objs[0] return r def test_enabled(self): SilkyConfig().SILKY_META = True r = self._execute_request() self.assertTrue(r.meta_time is not None or r.meta_num_queries is not None or r.meta_time_spent_queries is not None) def test_disabled(self): SilkyConfig().SILKY_META = False r = self._execute_request() self.assertFalse(r.meta_time) ================================================ FILE: project/tests/test_db.py ================================================ """ Test profiling of DB queries without mocking, to catch possible incompatibility """ from django.shortcuts import reverse from django.test import Client, TestCase from silk.collector import DataCollector from silk.config import SilkyConfig from silk.models import Request from silk.profiling.profiler import silk_profile from .factories import BlindFactory class TestDbQueries(TestCase): @classmethod def setUpClass(cls): super().setUpClass() BlindFactory.create_batch(size=5) SilkyConfig().SILKY_META = False def test_profile_request_to_db(self): DataCollector().configure(Request(reverse('example_app:index'))) with silk_profile(name='test_profile'): resp = self.client.get(reverse('example_app:index')) DataCollector().profiles.values() assert len(resp.context['blinds']) == 5 def test_profile_request_to_db_with_constraints(self): DataCollector().configure(Request(reverse('example_app:create'))) resp = self.client.post(reverse('example_app:create'), {'name': 'Foo'}) self.assertEqual(resp.status_code, 302) class TestAnalyzeQueries(TestCase): @classmethod def setUpClass(cls): super().setUpClass() BlindFactory.create_batch(size=5) SilkyConfig().SILKY_META = False SilkyConfig().SILKY_ANALYZE_QUERIES = True @classmethod def tearDownClass(cls): super().tearDownClass() SilkyConfig().SILKLY_ANALYZE_QUERIES = False def test_analyze_queries(self): DataCollector().configure(Request(reverse('example_app:index'))) client = Client() with silk_profile(name='test_profile'): resp = client.get(reverse('example_app:index')) DataCollector().profiles.values() assert len(resp.context['blinds']) == 5 class TestAnalyzeQueriesExplainParams(TestAnalyzeQueries): @classmethod def setUpClass(cls): super().setUpClass() SilkyConfig().SILKY_EXPLAIN_FLAGS = {'verbose': True} @classmethod def tearDownClass(cls): super().tearDownClass() SilkyConfig().SILKY_EXPLAIN_FLAGS = None ================================================ FILE: project/tests/test_dynamic_profiling.py ================================================ from unittest.mock import patch from django.test import TestCase import silk from silk.profiling.dynamic import ( _get_module, _get_parent_module, profile_function_or_method, ) from .test_lib.assertion import dict_contains from .util import mock_data_collector class TestGetModule(TestCase): """test for _get_module""" def test_singular(self): module = _get_module('silk') self.assertEqual(module.__class__.__name__, 'module') self.assertEqual('silk', module.__name__) self.assertTrue(hasattr(module, 'models')) def test_dot(self): module = _get_module('silk.models') self.assertEqual(module.__class__.__name__, 'module') self.assertEqual('silk.models', module.__name__) self.assertTrue(hasattr(module, 'SQLQuery')) class TestGetParentModule(TestCase): """test for silk.tools._get_parent_module""" def test_singular(self): parent = _get_parent_module(silk) self.assertIsInstance(parent, dict) def test_dot(self): import silk.utils parent = _get_parent_module(silk.utils) self.assertEqual(parent, silk) class MyClass: def foo(self): pass def foo(): pass def source_file_name(): file_name = __file__ if file_name[-1] == 'c': file_name = file_name[:-1] return file_name class TestProfileFunction(TestCase): def test_method_as_str(self): # noinspection PyShadowingNames def foo(_): pass # noinspection PyUnresolvedReferences with patch.object(MyClass, 'foo', foo): profile_function_or_method('tests.test_dynamic_profiling', 'MyClass.foo', 'test') dc = mock_data_collector() with patch('silk.profiling.profiler.DataCollector', return_value=dc) as mock_DataCollector: MyClass().foo() self.assertEqual(mock_DataCollector.return_value.register_profile.call_count, 1) call_args = mock_DataCollector.return_value.register_profile.call_args[0][0] self.assertTrue(dict_contains({ 'func_name': foo.__name__, 'dynamic': True, 'file_path': source_file_name(), 'name': 'test', 'line_num': foo.__code__.co_firstlineno }, call_args)) def test_func_as_str(self): name = foo.__name__ line_num = foo.__code__.co_firstlineno profile_function_or_method('tests.test_dynamic_profiling', 'foo', 'test') dc = mock_data_collector() with patch('silk.profiling.profiler.DataCollector', return_value=dc) as mock_DataCollector: foo() self.assertEqual(mock_DataCollector.return_value.register_profile.call_count, 1) call_args = mock_DataCollector.return_value.register_profile.call_args[0][0] self.assertTrue(dict_contains({ 'func_name': name, 'dynamic': True, 'file_path': source_file_name(), 'name': 'test', 'line_num': line_num }, call_args)) ================================================ FILE: project/tests/test_encoding.py ================================================ import json from unittest.mock import Mock from django.test import TestCase from silk.model_factory import RequestModelFactory, ResponseModelFactory HTTP_CONTENT_TYPE = 'content-type' class TestEncodingForRequests(TestCase): """ Check that the RequestModelFactory deals with encodings correctly via charset """ def test_utf_plain(self): mock_request = Mock() mock_request.headers = {HTTP_CONTENT_TYPE: 'text/plain; charset=UTF-8'} mock_request.body = '语' mock_request.get = mock_request.headers.get factory = RequestModelFactory(mock_request) body, raw_body = factory.body() self.assertFalse(body) self.assertEqual(raw_body, mock_request.body) def test_plain(self): mock_request = Mock() mock_request.headers = {HTTP_CONTENT_TYPE: 'text/plain'} mock_request.body = 'sdfsdf' mock_request.get = mock_request.headers.get factory = RequestModelFactory(mock_request) body, raw_body = factory.body() self.assertFalse(body) self.assertEqual(raw_body, mock_request.body) def test_utf_json_not_encoded(self): mock_request = Mock() mock_request.headers = {HTTP_CONTENT_TYPE: 'application/json; charset=UTF-8'} d = {'x': '语'} mock_request.body = json.dumps(d) mock_request.get = mock_request.headers.get factory = RequestModelFactory(mock_request) body, raw_body = factory.body() self.assertDictEqual(json.loads(body), d) self.assertEqual(raw_body, mock_request.body) def test_utf_json_encoded(self): mock_request = Mock() mock_request.headers = {HTTP_CONTENT_TYPE: 'application/json; charset=UTF-8'} d = {'x': '语'} mock_request.body = json.dumps(d).encode('UTF-8') mock_request.get = mock_request.headers.get factory = RequestModelFactory(mock_request) body, raw_body = factory.body() self.assertDictEqual(json.loads(body), d) self.assertEqual(raw_body, mock_request.body.decode('UTF-8')) def test_utf_json_encoded_no_charset(self): """default to UTF-8""" mock_request = Mock() mock_request.headers = {HTTP_CONTENT_TYPE: 'application/json'} d = {'x': '语'} mock_request.body = json.dumps(d).encode('UTF-8') mock_request.get = mock_request.headers.get factory = RequestModelFactory(mock_request) body, raw_body = factory.body() self.assertDictEqual(json.loads(body), d) self.assertEqual(raw_body, mock_request.body.decode('UTF-8')) def test_invalid_encoding_json(self): mock_request = Mock() mock_request.headers = {HTTP_CONTENT_TYPE: 'application/json; charset=asdas-8'} d = {'x': '语'} mock_request.body = json.dumps(d).encode('UTF-8') mock_request.get = mock_request.headers.get factory = RequestModelFactory(mock_request) body, raw_body = factory.body() self.assertDictEqual(json.loads(body), d) self.assertEqual(raw_body, raw_body) class TestEncodingForResponse(TestCase): """ Check that the ResponseModelFactory deals with encodings correctly via charset """ def test_utf_plain(self): mock = Mock() mock.headers = {HTTP_CONTENT_TYPE: 'text/plain; charset=UTF-8'} mock.content = '语' mock.get = mock.headers.get factory = ResponseModelFactory(mock) body, content = factory.body() self.assertFalse(body) self.assertEqual(content, mock.content) def test_plain(self): mock = Mock() mock.headers = {HTTP_CONTENT_TYPE: 'text/plain'} mock.content = 'sdfsdf' mock.get = mock.headers.get factory = ResponseModelFactory(mock) body, content = factory.body() self.assertFalse(body) self.assertEqual(content, mock.content) def test_utf_json_not_encoded(self): mock = Mock() mock.headers = {HTTP_CONTENT_TYPE: 'application/json; charset=UTF-8'} d = {'x': '语'} mock.content = json.dumps(d) mock.get = mock.headers.get factory = ResponseModelFactory(mock) body, content = factory.body() self.assertDictEqual(json.loads(body), d) self.assertEqual(content, mock.content) def test_utf_json_encoded(self): mock = Mock() mock.headers = {HTTP_CONTENT_TYPE: 'application/json; charset=UTF-8'} d = {'x': '语'} mock.content = json.dumps(d) mock.get = mock.headers.get factory = ResponseModelFactory(mock) body, content = factory.body() self.assertDictEqual(json.loads(body), d) self.assertEqual(content, mock.content) def test_utf_json_encoded_no_charset(self): """default to UTF-8""" mock = Mock() mock.headers = {HTTP_CONTENT_TYPE: 'application/json'} d = {'x': '语'} mock.content = json.dumps(d) mock.get = mock.headers.get factory = ResponseModelFactory(mock) body, content = factory.body() self.assertDictEqual(json.loads(body), d) self.assertEqual(content, mock.content) def test_invalid_encoding_json(self): mock = Mock() mock.headers = {HTTP_CONTENT_TYPE: 'application/json; charset=asdas-8'} d = {'x': '语'} mock.content = json.dumps(d) mock.get = mock.headers.get factory = ResponseModelFactory(mock) body, content = factory.body() self.assertDictEqual(json.loads(body), d) self.assertEqual(mock.content, content) ================================================ FILE: project/tests/test_end_points.py ================================================ import random from django.db.models import Count, F from django.test import TestCase from django.urls import reverse from silk import models from silk.config import SilkyConfig from silk.middleware import silky_reverse from .test_lib.mock_suite import MockSuite class TestEndPoints(TestCase): """ Hit all the endpoints to check everything actually renders/no error 500s etc. Each test will ensure that an object with something to display is chosen to be rendered e.g. a request/profile that has queries """ @classmethod def setUpClass(cls): super().setUpClass() # We're not testing auth here. SilkyConfig().SILKY_AUTHORISATION = False SilkyConfig().SILKY_AUTHENTICATION = False mock_suite = MockSuite() for _ in range(0, 100): mock_suite.mock_request() def test_summary(self): response = self.client.get(silky_reverse('summary')) self.assertTrue(response.status_code == 200) def test_requests(self): response = self.client.get(silky_reverse('requests')) self.assertTrue(response.status_code == 200) def test_request_detail_on_get_request(self): request_id = random.choice( models.Request.objects.filter(method='GET').values_list('id', flat=True), ) response = self.client.get(silky_reverse('request_detail', kwargs={ 'request_id': request_id })) self.assertEqual(response.status_code, 200) def test_request_detail_on_post_request(self): request_id = random.choice( models.Request.objects.filter(method='POST').values_list('id', flat=True), ) response = self.client.get(silky_reverse('request_detail', kwargs={ 'request_id': request_id })) self.assertEqual(response.status_code, 200) def test_request_sql(self): request_query_data = random.choice( models.SQLQuery.objects .values('request_id') .filter(request_id__isnull=False) ) request_id = request_query_data['request_id'] base_url = silky_reverse('request_sql', kwargs={'request_id': request_id}) response = self.client.get(base_url) self.assertTrue(response.status_code == 200) # Test with valid page size response = self.client.get(base_url + "?per_page=100") self.assertTrue(response.status_code == 200) # Test with invalid page size response = self.client.get(base_url + "?per_page=notanumber") self.assertTrue(response.status_code == 200) def test_request_sql_detail(self): kwargs = random.choice( models.SQLQuery.objects .annotate(sql_id=F('id')) .values('sql_id', 'request_id') .filter(request_id__isnull=False) ) response = self.client.get(silky_reverse('request_sql_detail', kwargs=kwargs)) self.assertTrue(response.status_code == 200) def test_raw(self): request_query_data = random.choice( models.Request.objects .values('id') .filter(body__isnull=False) ) request_id = request_query_data['id'] url = reverse('silk:raw', kwargs={ 'request_id': request_id }) + '?typ=request&subtyp=processed' response = self.client.get(url) code = response.status_code self.assertTrue(code == 200) def test_request_profiling(self): request_id = random.choice( models.Profile.objects .values('request_id') .filter(request_id__isnull=False) ) response = self.client.get(silky_reverse('request_profiling', kwargs=request_id)) self.assertTrue(response.status_code == 200) def test_request_profile_detail(self): kwargs = random.choice( models.Profile.objects .annotate(profile_id=F('id')) .values('profile_id', 'request_id') .filter(request_id__isnull=False) ) response = self.client.get(silky_reverse('request_profile_detail', kwargs=kwargs)) self.assertTrue(response.status_code == 200) def test_request_and_profile_sql(self): kwargs = random.choice( models.Profile.objects .annotate(num=Count('queries'), profile_id=F('id')) .values('profile_id', 'request_id') .filter(request_id__isnull=False, num__gt=0) ) response = self.client.get(silky_reverse('request_and_profile_sql', kwargs=kwargs)) self.assertTrue(response.status_code == 200) def test_request_and_profile_sql_detail(self): random_profile = random.choice( models.Profile.objects .annotate(num=Count('queries'), profile_id=F('id')) .values('profile_id', 'request_id') .filter(request_id__isnull=False, num__gt=0) ) random_sql_query = random.choice( models.SQLQuery.objects .annotate(sql_id=F('id')) .values('sql_id') .filter(profiles__id=random_profile['profile_id']) ) kwargs = {} kwargs.update(random_profile) kwargs.update(random_sql_query) response = self.client.get(silky_reverse('request_and_profile_sql_detail', kwargs=kwargs)) self.assertTrue(response.status_code == 200) def test_profile_detail(self): profile_query_data = random.choice(models.Profile.objects.values('id')) profile_id = profile_query_data['id'] response = self.client.get(silky_reverse('profile_detail', kwargs={ 'profile_id': profile_id })) self.assertTrue(response.status_code == 200) def test_profile_sql(self): profile_query_data = random.choice( models.Profile.objects .annotate(num=Count('queries')) .values('id') .filter(num__gt=0) ) profile_id = profile_query_data['id'] response = self.client.get(silky_reverse('profile_sql', kwargs={'profile_id': profile_id})) self.assertTrue(response.status_code == 200) def test_profile_sql_detail(self): profile_query_data = random.choice( models.Profile.objects .annotate(num=Count('queries')) .values('id') .filter(num__gt=0) ) profile_id = profile_query_data['id'] sql_id = random.choice(models.SQLQuery.objects.filter(profiles=profile_id)).pk response = self.client.get(silky_reverse('profile_sql_detail', kwargs={'profile_id': profile_id, 'sql_id': sql_id})) self.assertTrue(response.status_code == 200) def test_profiling(self): response = self.client.get(silky_reverse('profiling')) self.assertTrue(response.status_code == 200) ================================================ FILE: project/tests/test_execute_sql.py ================================================ from unittest.mock import Mock, NonCallableMagicMock, NonCallableMock, patch from django.test import TestCase from django.utils.encoding import force_str from silk.collector import DataCollector from silk.models import Request, SQLQuery from silk.sql import execute_sql from .util import delete_all_models _simple_mock_query_sql = 'SELECT * FROM table_name WHERE column1 = %s' _simple_mock_query_params = ('asdf',) _non_unicode_binary_mock_query_params = (b'\x0a\x00\x00\xff',) _unicode_binary_mock_query_params = ('🫠'.encode(),) def mock_sql(mock_query_params): mock_sql_query = Mock(spec_set=['_execute_sql', 'query', 'as_sql', 'connection']) mock_sql_query._execute_sql = Mock() mock_sql_query.query = NonCallableMock(spec_set=['model']) mock_sql_query.query.model = Mock() mock_sql_query.as_sql = Mock(return_value=(_simple_mock_query_sql, mock_query_params)) mock_sql_query.connection = NonCallableMock( spec_set=['cursor', 'features', 'ops'], cursor=Mock( spec_set=['__call__'], return_value=NonCallableMagicMock(spec_set=['__enter__', '__exit__', 'execute']) ), features=NonCallableMock( spec_set=['supports_explaining_query_execution'], supports_explaining_query_execution=True ), ops=NonCallableMock(spec_set=['explain_query_prefix'], explain_query_prefix=Mock(return_value='')), ) return mock_sql_query, mock_query_params class BaseTestCase(TestCase): def tearDown(self): DataCollector().stop_python_profiler() def call_execute_sql(self, request, mock_query_params): DataCollector().configure(request=request) delete_all_models(SQLQuery) self.query_string = _simple_mock_query_sql self.mock_sql, self.query_params = mock_sql(mock_query_params) self.kwargs = { 'one': 1, 'two': 2 } self.args = [1, 2] execute_sql(self.mock_sql, *self.args, **self.kwargs) class TestCallNoRequest(BaseTestCase): def setUp(self): super().setUp() self.call_execute_sql(None, _simple_mock_query_params) def test_called(self): self.mock_sql._execute_sql.assert_called_once_with(*self.args, **self.kwargs) def test_count(self): self.assertEqual(0, len(DataCollector().queries)) class TestCallRequest(BaseTestCase): def test_query_simple(self): self.call_execute_sql(Request(), _simple_mock_query_params) self.mock_sql._execute_sql.assert_called_once_with(*self.args, **self.kwargs) self.assertEqual(1, len(DataCollector().queries)) query = list(DataCollector().queries.values())[0] expected = self.query_string % tuple(force_str(param) for param in self.query_params) self.assertEqual(query['query'], expected) def test_query_unicode(self): self.call_execute_sql(Request(), _unicode_binary_mock_query_params) self.mock_sql._execute_sql.assert_called_once_with(*self.args, **self.kwargs) self.assertEqual(1, len(DataCollector().queries)) query = list(DataCollector().queries.values())[0] expected = self.query_string % tuple(force_str(param) for param in self.query_params) self.assertEqual(query['query'], expected) def test_query_non_unicode(self): self.call_execute_sql(Request(), _non_unicode_binary_mock_query_params) self.mock_sql._execute_sql.assert_called_once_with(*self.args, **self.kwargs) self.assertEqual(0, len(DataCollector().queries)) class TestCallSilky(BaseTestCase): def test_no_effect(self): DataCollector().configure() sql, _ = mock_sql(_simple_mock_query_params) sql.query.model = NonCallableMagicMock(spec_set=['__module__']) sql.query.model.__module__ = 'silk.models' # No SQLQuery models should be created for silk requests for obvious reasons with patch('silk.sql.DataCollector', return_value=Mock()) as mock_DataCollector: execute_sql(sql) self.assertFalse(mock_DataCollector().register_query.call_count) class TestCollectorInteraction(BaseTestCase): def _query(self): try: query = list(DataCollector().queries.values())[0] except IndexError: self.fail('No queries created') return query def test_request(self): DataCollector().configure(request=Request.objects.create(path='/path/to/somewhere')) sql, _ = mock_sql(_simple_mock_query_params) execute_sql(sql) query = self._query() self.assertEqual(query['request'], DataCollector().request) def test_registration(self): DataCollector().configure(request=Request.objects.create(path='/path/to/somewhere')) sql, _ = mock_sql(_simple_mock_query_params) execute_sql(sql) query = self._query() self.assertIn(query, DataCollector().queries.values()) def test_explain_simple(self): DataCollector().configure(request=Request.objects.create(path='/path/to/somewhere')) sql, params = mock_sql(_simple_mock_query_params) prefix = "EXPLAIN" mock_cursor = sql.connection.cursor.return_value.__enter__.return_value sql.connection.ops.explain_query_prefix.return_value = prefix execute_sql(sql) self.assertNotIn(prefix, params) mock_cursor.execute.assert_called_once_with(f"{prefix} {_simple_mock_query_sql}", params) def test_explain_unicode(self): DataCollector().configure(request=Request.objects.create(path='/path/to/somewhere')) sql, params = mock_sql(_unicode_binary_mock_query_params) prefix = "EXPLAIN" mock_cursor = sql.connection.cursor.return_value.__enter__.return_value sql.connection.ops.explain_query_prefix.return_value = prefix execute_sql(sql) self.assertNotIn(prefix, params) mock_cursor.execute.assert_called_once_with(f"{prefix} {_simple_mock_query_sql}", params) def test_explain_non_unicode(self): DataCollector().configure(request=Request.objects.create(path='/path/to/somewhere')) sql, params = mock_sql(_non_unicode_binary_mock_query_params) prefix = "EXPLAIN" mock_cursor = sql.connection.cursor.return_value.__enter__.return_value sql.connection.ops.explain_query_prefix.return_value = prefix execute_sql(sql) self.assertNotIn(prefix, params) self.assertFalse(mock_cursor.execute.called) ================================================ FILE: project/tests/test_filters.py ================================================ import calendar import random from datetime import datetime, timedelta, timezone from itertools import groupby from math import floor from django.test import TestCase from django.utils import timezone as django_timezone from silk import models from silk.request_filters import ( AfterDateFilter, BeforeDateFilter, FunctionNameFilter, MethodFilter, NameFilter, NumQueriesFilter, OverallTimeFilter, PathFilter, SecondsFilter, StatusCodeFilter, TimeSpentOnQueriesFilter, ViewNameFilter, ) from .test_lib.mock_suite import MockSuite from .util import delete_all_models mock_suite = MockSuite() class TestRequestFilters(TestCase): @classmethod def setUpClass(cls): super().setUpClass() def _time_stamp(self, dt): return calendar.timegm(dt.utctimetuple()) def test_seconds_filter(self): requests = [mock_suite.mock_request() for _ in range(0, 10)] n = 0 for r in requests: r.start_time = django_timezone.now() - timedelta(seconds=n) r.save() n += 1 requests = models.Request.objects.filter(SecondsFilter(5)) for r in requests: dt = r.start_time seconds = self._time_stamp(django_timezone.now()) - self._time_stamp(dt) self.assertTrue(seconds < 6) # 6 to give a bit of leeway in case takes too long def test_view_name_filter(self): requests = [mock_suite.mock_request() for _ in range(0, 10)] r = random.choice(requests) view_name = r.view_name requests = models.Request.objects.filter(ViewNameFilter(view_name)) for r in requests: self.assertTrue(r.view_name == view_name) def test_path_filter(self): requests = [mock_suite.mock_request() for _ in range(0, 10)] r = random.choice(requests) path = r.path requests = models.Request.objects.filter(PathFilter(path)) for r in requests: self.assertTrue(r.path == path) def test_num_queries_filter(self): requests = [mock_suite.mock_request() for _ in range(0, 10)] counts = sorted(x.queries.count() for x in requests) c = counts[int(floor(len(counts) / 2))] num_queries_filter = NumQueriesFilter(c) query_set = models.Request.objects.all() query_set = num_queries_filter.contribute_to_query_set(query_set) filtered = query_set.filter(num_queries_filter) for f in filtered: self.assertGreaterEqual(f.queries.count(), c) def test_time_spent_queries_filter(self): requests = [mock_suite.mock_request() for _ in range(0, 10)] time_taken = sorted(sum(q.time_taken for q in x.queries.all()) for x in requests) c = time_taken[int(floor(len(time_taken) / 2))] time_taken_filter = TimeSpentOnQueriesFilter(c) query_set = models.Request.objects.all() query_set = time_taken_filter.contribute_to_query_set(query_set) filtered = query_set.filter(time_taken_filter) for f in filtered: self.assertGreaterEqual(sum(q.time_taken for q in f.queries.all()), c) def test_time_spent_filter(self): requests = [mock_suite.mock_request() for _ in range(0, 10)] time_taken = sorted(x.time_taken for x in requests) c = time_taken[int(floor(len(time_taken) / 2))] time_taken_filter = OverallTimeFilter(c) query_set = models.Request.objects.all() query_set = time_taken_filter.contribute_to_query_set(query_set) filtered = query_set.filter(time_taken_filter) for f in filtered: self.assertGreaterEqual(round(f.time_taken), round(c)) def test_status_code_filter(self): requests = [mock_suite.mock_request() for _ in range(0, 50)] requests = sorted(requests, key=lambda x: x.response.status_code) by_status_code = groupby(requests, key=lambda x: x.response.status_code) for status_code, expected in by_status_code: status_code_filter = StatusCodeFilter(status_code) query_set = models.Request.objects.all() query_set = status_code_filter.contribute_to_query_set(query_set) filtered = query_set.filter(status_code_filter) self.assertEqual(len(list(expected)), filtered.count()) def test_method_filter(self): requests = [mock_suite.mock_request() for _ in range(0, 50)] requests = sorted(requests, key=lambda x: x.method) by_method = groupby(requests, key=lambda x: x.method) for method, expected in by_method: method_filter = MethodFilter(method) query_set = models.Request.objects.all() query_set = method_filter.contribute_to_query_set(query_set) filtered = query_set.filter(method_filter) self.assertEqual(len(list(expected)), filtered.count()) class TestRequestAfterDateFilter(TestCase): def assertFilter(self, dt, f): requests = models.Request.objects.filter(f) for r in requests: self.assertTrue(r.start_time > dt) @classmethod def setUpClass(cls): super().setUpClass() cls.requests = [mock_suite.mock_request() for _ in range(0, 10)] def test_after_date_filter(self): r = random.choice(self.requests) dt = r.start_time f = AfterDateFilter(dt) self.assertFilter(dt, f) def test_after_date_filter_str(self): r = random.choice(self.requests) dt = r.start_time fmt = AfterDateFilter.fmt dt_str = dt.strftime(fmt) date_filter = AfterDateFilter f = date_filter(dt_str) new_dt = datetime.strptime(dt_str, fmt) new_dt = django_timezone.make_aware(new_dt, timezone.utc) self.assertFilter(new_dt, f) class TestRequestBeforeDateFilter(TestCase): def assertFilter(self, dt, f): requests = models.Request.objects.filter(f) for r in requests: self.assertTrue(r.start_time < dt) @classmethod def setUpClass(cls): super().setUpClass() cls.requests = [mock_suite.mock_request() for _ in range(0, 10)] def test_before_date_filter(self): r = random.choice(self.requests) dt = r.start_time f = BeforeDateFilter(dt) self.assertFilter(dt, f) def test_before_date_filter_str(self): r = random.choice(self.requests) dt = r.start_time fmt = BeforeDateFilter.fmt dt_str = dt.strftime(fmt) date_filter = BeforeDateFilter f = date_filter(dt_str) new_dt = datetime.strptime(dt_str, fmt) new_dt = django_timezone.make_aware(new_dt, timezone.utc) self.assertFilter(new_dt, f) class TestProfileFilters(TestCase): def setUp(self): delete_all_models(models.Profile) def test_name_filter(self): profiles = mock_suite.mock_profiles(n=10) p = random.choice(profiles) name = p.name requests = models.Profile.objects.filter(NameFilter(name)) for p in requests: self.assertTrue(p.name == name) def test_function_name_filter(self): profiles = mock_suite.mock_profiles(n=10) p = random.choice(profiles) func_name = p.func_name requests = models.Profile.objects.filter(FunctionNameFilter(func_name)) for p in requests: self.assertTrue(p.func_name == func_name) def test_num_queries_filter(self): profiles = mock_suite.mock_profiles(n=10) counts = sorted(x.queries.count() for x in profiles) c = counts[int(floor(len(counts) / 2))] num_queries_filter = NumQueriesFilter(c) query_set = models.Profile.objects.all() query_set = num_queries_filter.contribute_to_query_set(query_set) filtered = query_set.filter(num_queries_filter) for f in filtered: self.assertGreaterEqual(f.queries.count(), c) def test_time_spent_queries_filter(self): profiles = mock_suite.mock_profiles(n=10) time_taken = sorted(sum(q.time_taken for q in x.queries.all()) for x in profiles) c = time_taken[int(floor(len(time_taken) / 2))] time_taken_filter = TimeSpentOnQueriesFilter(c) query_set = models.Profile.objects.all() query_set = time_taken_filter.contribute_to_query_set(query_set) filtered = query_set.filter(time_taken_filter) for f in filtered: self.assertGreaterEqual(sum(q.time_taken for q in f.queries.all()), c) def test_time_spent_filter(self): profiles = [mock_suite.mock_request() for _ in range(0, 10)] time_taken = sorted(x.time_taken for x in profiles) c = time_taken[int(floor(len(time_taken) / 2))] time_taken_filter = OverallTimeFilter(c) query_set = models.Profile.objects.all() query_set = time_taken_filter.contribute_to_query_set(query_set) filtered = query_set.filter(time_taken_filter) for f in filtered: self.assertGreaterEqual(f.time_taken, c) ================================================ FILE: project/tests/test_lib/__init__.py ================================================ __author__ = 'mtford' ================================================ FILE: project/tests/test_lib/assertion.py ================================================ def dict_contains(child_dict, parent_dict): for key, value in child_dict.items(): if key not in parent_dict: return False if parent_dict[key] != value: return False return True ================================================ FILE: project/tests/test_lib/mock_suite.py ================================================ import json import os import random import traceback from datetime import timedelta from django.core import management from django.utils import timezone from silk import models from silk.models import Profile, SQLQuery class MockSuite: """ Provides some fake data to play around with. Also useful for testing """ methods = ['GET', 'POST', 'PUT', 'PATCH', 'HEAD', 'OPTIONS'] path_components = ['path', 'to', 'somewhere', 'around', 'here', 'bobs', 'your', 'uncle'] status_codes = [200, 201, 300, 301, 302, 403, 404, 500] profile_names = ['slow_bit_of_code', 'terrible_dependency', 'what_on_earth_is_this_code_doing'] file_path = [os.path.realpath(__file__)] func_names = ['', '', '', 'foo', 'bar'] view_names = ['app:blah', 'index', 'root', 'xxx:xyx'] sql_queries = [''' SELECT Book.title AS Title, COUNT(*) AS Authors FROM Book JOIN Book_author ON Book.isbn = Book_author.isbn GROUP BY Book.title; ''', ''' SELECT * FROM table ''', ''' SELECT * FROM Book WHERE price > 100.00 ORDER BY title; ''', ''' SELECT title, COUNT(*) AS Authors FROM Book NATURAL JOIN Book_author GROUP BY title; ''', ''' SELECT A.Col1, A.Col2, B.Col1,B.Col2 FROM (SELECT RealTableZ.Col1, RealTableY.Col2, RealTableY.ID AS ID FROM RealTableZ LEFT OUTER JOIN RealTableY ON RealTableZ.ForeignKeyY=RealTableY.ID WHERE RealTableY.Col11>14 ) AS B INNER JOIN A ON A.ForeignKeyY=B.ID '''] response_content_types = ['text/html', 'application/json', 'text/css'] response_content = { 'text/html': [''], 'text/css': ['#blah {font-weight: bold}'], 'application/json': ['[1, 2, 3]'] } request_content_types = ['application/json'] request_content = { 'application/json': ['{"blah": 5}'] } def _random_method(self): return random.choice(self.methods) def _random_path(self): num_components = random.randint(1, 5) return '/' + '/'.join(random.sample(self.path_components, num_components)) + '/' def _random_query(self): return random.choice(self.sql_queries) def mock_sql_queries(self, request=None, profile=None, n=1, as_dict=False): start_time, end_time = self._random_time() queries = [] for _ in range(0, n): tb = ''.join(reversed(traceback.format_stack())) d = { 'query': self._random_query(), 'start_time': start_time, 'end_time': end_time, 'request': request, 'traceback': tb } if as_dict: queries.append(d) else: query = SQLQuery.objects.create(**d) queries.append(query) if profile: if as_dict: for q in queries: profile['queries'].append(q) else: profile.queries.set(queries) return queries def mock_profile(self, request=None): start_time, end_time = self._random_time() dynamic = random.choice([True, False]) profile = Profile.objects.create(start_time=start_time, end_time=end_time, request=request, name=random.choice(self.profile_names), file_path=random.choice(self.file_path), line_num=3, func_name=random.choice(self.func_names), dynamic=dynamic, end_line_num=6 if dynamic else None, exception_raised=random.choice([True, False]) ) self.mock_sql_queries(profile=profile, n=random.randint(0, 10)) return profile def mock_profiles(self, request=None, n=1): profiles = [] for _ in range(0, n): profile = self.mock_profile(request) profiles.append(profile) return profiles def _random_time(self): start_time = timezone.now() duration = timedelta(milliseconds=random.randint(0, 3000)) end_time = start_time + duration return start_time, end_time def mock_request(self): start_time, end_time = self._random_time() num_sql_queries = random.randint(0, 20) request_content_type = random.choice(self.request_content_types) request_body = random.choice(self.request_content[request_content_type]) time_taken = end_time - start_time time_taken = time_taken.total_seconds() request = models.Request.objects.create(method=self._random_method(), path=self._random_path(), num_sql_queries=num_sql_queries, start_time=start_time, end_time=end_time, view_name=random.choice(self.view_names), time_taken=time_taken, encoded_headers=json.dumps({'content-type': request_content_type}), body=request_body) response_content_type = random.choice(self.response_content_types) response_body = random.choice(self.response_content[response_content_type]) models.Response.objects.create(request=request, status_code=random.choice(self.status_codes), encoded_headers=json.dumps({'content-type': response_content_type}), body=response_body) self.mock_sql_queries(request=request, n=num_sql_queries) self.mock_profiles(request, random.randint(0, 2)) return request if __name__ == '__main__': management.call_command('flush', interactive=False) requests = [MockSuite().mock_request() for _ in range(0, 100)] ================================================ FILE: project/tests/test_models.py ================================================ import datetime import uuid from django.core.management import call_command from django.test import TestCase, override_settings from freezegun import freeze_time from silk import models from silk.config import SilkyConfig from silk.storage import ProfilerResultStorage from .factories import RequestMinFactory, ResponseFactory, SQLQueryFactory # TODO test atomicity # http://stackoverflow.com/questions/13397038/uuid-max-character-length # UUID_MAX_LENGTH = 36 # TODO move to separate file test and collection it self class CaseInsensitiveDictionaryTest: pass class RequestTest(TestCase): def setUp(self): self.obj = RequestMinFactory.create() self.max_percent = SilkyConfig().SILKY_MAX_RECORDED_REQUESTS_CHECK_PERCENT self.max_requests = SilkyConfig().SILKY_MAX_RECORDED_REQUESTS def tearDown(self): SilkyConfig().SILKY_MAX_RECORDED_REQUESTS_CHECK_PERCENT = self.max_percent SilkyConfig().SILKY_MAX_RECORDED_REQUESTS = self.max_requests def test_uuid_is_primary_key(self): self.assertIsInstance(self.obj.id, uuid.UUID) @freeze_time('2016-01-01 12:00:00') def test_start_time_field_default(self): obj = RequestMinFactory.create() self.assertEqual(obj.start_time, datetime.datetime(2016, 1, 1, 12, 0, 0, tzinfo=datetime.timezone.utc)) def test_total_meta_time_if_have_no_meta_and_queries_time(self): self.assertEqual(self.obj.total_meta_time, 0) def test_total_meta_time_if_have_meta_time_spent_queries(self): obj = RequestMinFactory.create(meta_time_spent_queries=10.5) self.assertEqual(obj.total_meta_time, 10.5) def test_total_meta_time_if_meta_time(self): obj = RequestMinFactory.create(meta_time=3.3) self.assertEqual(obj.total_meta_time, 3.3) def test_total_meta_if_self_have_it_meta_and_queries_time(self): obj = RequestMinFactory.create(meta_time=3.3, meta_time_spent_queries=10.5) self.assertEqual(obj.total_meta_time, 13.8) def test_time_spent_on_sql_queries_if_has_no_related_SQLQueries(self): self.assertEqual(self.obj.time_spent_on_sql_queries, 0) def test_time_spent_on_sql_queries_if_has_related_SQLQueries_with_no_time_taken(self): query = SQLQueryFactory() self.obj.queries.add(query) self.assertEqual(query.time_taken, None) # No exception should be raised, and the result should be 0.0 self.assertEqual(self.obj.time_spent_on_sql_queries, 0.0) def test_time_spent_on_sql_queries_if_has_related_SQLQueries_and_time_taken(self): query1 = SQLQueryFactory(time_taken=3.5) query2 = SQLQueryFactory(time_taken=1.5) RequestMinFactory().queries.add(query1, query2) self.assertEqual(self.obj.time_spent_on_sql_queries, 0) def test_headers_if_has_no_encoded_headers(self): self.assertIsInstance(self.obj.headers, models.CaseInsensitiveDictionary) self.assertFalse(self.obj.headers) def test_headers_if_has_encoded_headers(self): self.obj.encoded_headers = '{"some-header": "some_data"}' self.assertIsInstance(self.obj.headers, models.CaseInsensitiveDictionary) self.assertDictEqual(self.obj.headers, {'some-header': 'some_data'}) def test_content_type_if_no_headers(self): self.assertEqual(self.obj.content_type, None) def test_content_type_if_no_specific_content_type(self): self.obj.encoded_headers = '{"foo": "some_data"}' self.assertEqual(self.obj.content_type, None) def test_content_type_if_header_have_content_type(self): self.obj.encoded_headers = '{"content-type": "some_data"}' self.assertEqual(self.obj.content_type, "some_data") def test_garbage_collect(self): self.assertTrue(models.Request.objects.filter(id=self.obj.id).exists()) SilkyConfig().SILKY_MAX_RECORDED_REQUESTS_CHECK_PERCENT = 100 SilkyConfig().SILKY_MAX_RECORDED_REQUESTS = 0 models.Request.garbage_collect() self.assertFalse(models.Request.objects.filter(id=self.obj.id).exists()) def test_probabilistic_garbage_collect(self): self.assertTrue(models.Request.objects.filter(id=self.obj.id).exists()) SilkyConfig().SILKY_MAX_RECORDED_REQUESTS_CHECK_PERCENT = 0 SilkyConfig().SILKY_MAX_RECORDED_REQUESTS = 0 models.Request.garbage_collect() self.assertTrue(models.Request.objects.filter(id=self.obj.id).exists()) def test_force_garbage_collect(self): self.assertTrue(models.Request.objects.filter(id=self.obj.id).exists()) SilkyConfig().SILKY_MAX_RECORDED_REQUESTS_CHECK_PERCENT = 0 SilkyConfig().SILKY_MAX_RECORDED_REQUESTS = 0 models.Request.garbage_collect(force=True) self.assertFalse(models.Request.objects.filter(id=self.obj.id).exists()) def test_greedy_garbage_collect(self): for x in range(3): obj = models.Request(path='/', method='get') obj.save() self.assertEqual(models.Request.objects.count(), 4) SilkyConfig().SILKY_MAX_RECORDED_REQUESTS_CHECK_PERCENT = 50 SilkyConfig().SILKY_MAX_RECORDED_REQUESTS = 3 models.Request.garbage_collect(force=True) self.assertGreater(models.Request.objects.count(), 0) def test_save_if_have_no_raw_body(self): obj = models.Request(path='/some/path/', method='get') self.assertEqual(obj.raw_body, '') obj.save() self.assertEqual(obj.raw_body, '') def test_save_if_have_raw_body(self): obj = models.Request(path='/some/path/', method='get', raw_body='some text') obj.save() self.assertEqual(obj.raw_body, 'some text') def test_save_if_have_no_body(self): obj = models.Request(path='/some/path/', method='get') self.assertEqual(obj.body, '') obj.save() self.assertEqual(obj.body, '') def test_save_if_have_body(self): obj = models.Request(path='/some/path/', method='get', body='some text') obj.save() self.assertEqual(obj.body, 'some text') def test_save_if_have_no_end_time(self): obj = models.Request(path='/some/path/', method='get') self.assertEqual(obj.time_taken, None) obj.save() self.assertEqual(obj.time_taken, None) @freeze_time('2016-01-01 12:00:00') def test_save_if_have_end_time(self): date = datetime.datetime(2016, 1, 1, 12, 0, 3, tzinfo=datetime.timezone.utc) obj = models.Request(path='/some/path/', method='get', end_time=date) obj.save() self.assertEqual(obj.end_time, date) self.assertEqual(obj.time_taken, 3000.0) def test_prof_file_default_storage(self): obj = models.Request(path='/some/path/', method='get') self.assertEqual(obj.prof_file.storage.__class__, ProfilerResultStorage) class ResponseTest(TestCase): def setUp(self): self.obj = ResponseFactory.create() def test_uuid_is_primary_key(self): self.assertIsInstance(self.obj.id, uuid.UUID) def test_is_1to1_related_to_request(self): request = RequestMinFactory.create() resp = models.Response.objects.create(status_code=200, request=request) self.assertEqual(request.response, resp) def test_headers_if_has_no_encoded_headers(self): self.assertIsInstance(self.obj.headers, models.CaseInsensitiveDictionary) self.assertFalse(self.obj.headers) def test_headers_if_has_encoded_headers(self): self.obj.encoded_headers = '{"some-header": "some_data"}' self.assertIsInstance(self.obj.headers, models.CaseInsensitiveDictionary) self.assertDictEqual(self.obj.headers, {'some-header': 'some_data'}) def test_content_type_if_no_headers(self): self.assertEqual(self.obj.content_type, None) def test_content_type_if_no_specific_content_type(self): self.obj.encoded_headers = '{"foo": "some_data"}' self.assertEqual(self.obj.content_type, None) def test_content_type_if_header_have_content_type(self): self.obj.encoded_headers = '{"content-type": "some_data"}' self.assertEqual(self.obj.content_type, "some_data") class SQLQueryManagerTest(TestCase): def test_if_no_args_passed(self): pass def test_if_one_arg_passed(self): pass def if_a_few_args_passed(self): pass def if_objs_kw_arg_passed(self): pass def if_not_the_objs_kw_arg_passed(self): pass class SQLQueryTest(TestCase): def setUp(self): self.obj = SQLQueryFactory.create() self.end_time = datetime.datetime(2016, 1, 1, 12, 0, 5, tzinfo=datetime.timezone.utc) self.start_time = datetime.datetime(2016, 1, 1, 12, 0, 0, tzinfo=datetime.timezone.utc) @freeze_time('2016-01-01 12:00:00') def test_start_time_field_default(self): obj = SQLQueryFactory.create() self.assertEqual(obj.start_time, datetime.datetime(2016, 1, 1, 12, 0, 0, tzinfo=datetime.timezone.utc)) def test_is_m2o_related_to_request(self): request = RequestMinFactory() self.obj.request = request self.obj.save() self.assertIn(self.obj, request.queries.all()) def test_query_manager_instance(self): self.assertIsInstance(models.SQLQuery.objects, models.SQLQueryManager) def test_traceback_ln_only(self): self.obj.traceback = """Traceback (most recent call last): File "/home/user/some_script.py", line 10, in some_func pass File "/usr/lib/python2.7/bdb.py", line 20, in trace_dispatch return self.dispatch_return(frame, arg) File "/usr/lib/python2.7/bdb.py", line 30, in dispatch_return if self.quitting: raise BdbQuit BdbQuit""" output = ('Traceback (most recent call last):\n' ' pass\n' ' return self.dispatch_return(frame, arg)\n' ' if self.quitting: raise BdbQuit') self.assertEqual(self.obj.traceback_ln_only, output) def test_formatted_query_if_no_query(self): self.obj.query = "" self.obj.formatted_query def test_formatted_query_if_has_a_query(self): query = """SELECT Book.title AS Title, COUNT(*) AS Authors FROM Book JOIN Book_author ON Book.isbn = Book_author.isbn GROUP BY Book.title;""" self.obj.query = query self.obj.formatted_query def test_num_joins_if_no_joins_in_query(self): query = """SELECT Book.title AS Title, COUNT(*) AS Authors FROM Book GROUP BY Book.title;""" self.obj.query = query self.assertEqual(self.obj.num_joins, 0) def test_num_joins_if_joins_in_query(self): query = """SELECT p.id FROM Person p JOIN address a ON p.Id = a.Person_ID JOIN address_type at ON a.Type_ID = at.Id JOIN `option` o ON p.Id = o.person_Id JOIN option_address_type oat ON o.id = oat.option_id WHERE a.country_id = 1 AND at.id <> oat.type_id;""" self.obj.query = query self.assertEqual(self.obj.num_joins, 4) def test_num_joins_if_no_joins_in_query_but_this_word_searched(self): query = """SELECT Book.title FROM Book WHERE Book.title=`Join the dark side, Luke!`;""" self.obj.query = query self.assertEqual(self.obj.num_joins, 0) def test_num_joins_if_multiple_statement_in_query(self): query = """SELECT Book.title FROM Book WHERE Book.title=`Join the dark side, Luke!`; SELECT Book.joiner FROM Book LEFT OUTER JOIN joined ON Book.joiner = joined.joiner INNER JOIN joined ON Book.joiner = joined.joiner WHERE Book.joiner='Join i am'""" self.obj.query = query self.assertEqual(self.obj.num_joins, 2) def test_tables_involved_if_no_query(self): self.obj.query = '' self.assertEqual(self.obj.tables_involved, []) def test_tables_involved_if_query_has_only_a_from_token(self): query = """SELECT * FROM Book;""" self.obj.query = query self.assertEqual(self.obj.tables_involved, ['Book;']) def test_tables_involved_if_query_has_a_join_token(self): query = """SELECT p.id FROM Person p JOIN Address a ON p.Id = a.Person_ID;""" self.obj.query = query self.assertEqual(self.obj.tables_involved, ['Person', 'Address']) def test_tables_involved_if_query_has_an_as_token(self): query = 'SELECT Book.title AS Title FROM Book GROUP BY Book.title;' self.obj.query = query self.assertEqual(self.obj.tables_involved, ['Title', 'Book']) # FIXME bug, not a feature def test_tables_involved_check_with_fake_a_from_token(self): query = """SELECT * FROM Book WHERE Book.title=`EVIL FROM WITHIN`;""" self.obj.query = query self.assertEqual(self.obj.tables_involved, ['Book', 'WITHIN`;']) # FIXME bug, not a feature def test_tables_involved_check_with_fake_a_join_token(self): query = """SELECT * FROM Book WHERE Book.title=`Luke, join the dark side!`;""" self.obj.query = query self.assertEqual(self.obj.tables_involved, ['Book', 'the']) # FIXME bug, not a feature def test_tables_involved_check_with_fake_an_as_token(self): query = """SELECT * FROM Book WHERE Book.title=`AS SOON AS POSIABLE`;""" self.obj.query = query self.assertEqual(self.obj.tables_involved, ['Book', 'POSIABLE`;']) def test_tables_involved_if_query_has_subquery(self): query = '''SELECT A.Col1, A.Col2, B.Col1,B.Col2 FROM (SELECT RealTableZ.Col1, RealTableY.Col2, RealTableY.ID AS ID FROM RealTableZ LEFT OUTER JOIN RealTableY ON RealTableZ.ForeignKeyY=RealTableY.ID WHERE RealTableY.Col11>14 ) AS B INNER JOIN A ON A.ForeignKeyY=B.ID;''' self.obj.query = query self.assertEqual(self.obj.tables_involved, ['ID', 'RealTableZ', 'RealTableY', 'B', 'A']) # FIXME bug, not a feature def test_tables_involved_if_query_has_django_aliase_on_column_names(self): query = 'SELECT foo AS bar FROM some_table;' self.obj.query = query self.assertEqual(self.obj.tables_involved, ['bar', 'some_table;']) def test_tables_involved_if_query_has_update_token(self): query = """UPDATE Book SET title = 'New Title' WHERE id = 1;""" self.obj.query = query self.assertEqual(self.obj.tables_involved, ['Book']) def test_tables_involved_in_complex_update_query(self): query = '''UPDATE Person p SET p.name = (SELECT c.name FROM Company c WHERE c.id = p.company_id), p.salary = p.salary * 1.1 FROM Department d WHERE p.department_id = d.id AND d.budget > 100000; ''' self.obj.query = query self.assertEqual(self.obj.tables_involved, ['Person', 'Company', 'Department']) def test_tables_involved_in_update_with_subquery(self): query = '''UPDATE Employee e SET e.bonus = (SELECT AVG(salary) FROM Employee WHERE department_id = e.department_id) WHERE e.performance = 'excellent'; ''' self.obj.query = query self.assertEqual(self.obj.tables_involved, ['Employee', 'Employee']) def test_save_if_no_end_and_start_time(self): obj = SQLQueryFactory.create() self.assertEqual(obj.time_taken, None) @freeze_time('2016-01-01 12:00:00') def test_save_if_has_end_time(self): # datetime.datetime(2016, 1, 1, 12, 0, 5, tzinfo=datetime.timezone.utc) obj = SQLQueryFactory.create(end_time=self.end_time) self.assertEqual(obj.time_taken, 5000.0) @freeze_time('2016-01-01 12:00:00') def test_save_if_has_start_time(self): obj = SQLQueryFactory.create(start_time=self.start_time) self.assertEqual(obj.time_taken, None) def test_save_if_has_end_and_start_time(self): obj = SQLQueryFactory.create(start_time=self.start_time, end_time=self.end_time) self.assertEqual(obj.time_taken, 5000.0) def test_save_if_has_pk_and_request(self): self.obj.request = RequestMinFactory.create() self.obj.save() self.assertEqual(self.obj.request.num_sql_queries, 0) def test_save_if_has_no_pk(self): obj = SQLQueryFactory.build(start_time=self.start_time, end_time=self.end_time) obj.request = RequestMinFactory.create() obj.save() self.assertEqual(obj.request.num_sql_queries, 1) # should not rise def test_save_if_has_no_request(self): obj = SQLQueryFactory.build(start_time=self.start_time, end_time=self.end_time) obj.save() # FIXME a bug def test_delete_if_no_related_requests(self): with self.assertRaises(AttributeError): self.obj.delete() # self.assertNotIn(self.obj, models.SQLQuery.objects.all()) def test_delete_if_has_request(self): self.obj.request = RequestMinFactory.create() self.obj.save() self.obj.delete() self.assertNotIn(self.obj, models.SQLQuery.objects.all()) class NoPendingMigrationsTest(TestCase): """ Test if proper migrations are added and the models state is consistent. It should make sure that no new migrations are created for this app, when end-user runs `makemigrations` command. """ def test_no_pending_migrations(self): call_command("makemigrations", "silk", "--check", "--dry-run") @override_settings(DEFAULT_AUTO_FIELD='django.db.models.BigAutoField') def test_check_with_overridden_default_auto_field(self): """ Test with `BigAutoField` set as `DEFAULT_AUTO_FIELD` - which is default when generating proj with Django 3.2. """ self.test_no_pending_migrations() class BaseProfileTest(TestCase): pass class ProfileTest(TestCase): pass ================================================ FILE: project/tests/test_multipart_forms.py ================================================ from unittest.mock import Mock from django.test import TestCase from django.urls import reverse from silk.model_factory import RequestModelFactory, multipart_form class TestMultipartForms(TestCase): def test_no_max_request(self): mock_request = Mock() mock_request.headers = {'content-type': multipart_form} mock_request.GET = {} mock_request.path = reverse('silk:requests') mock_request.method = 'post' mock_request.body = Mock() request_model = RequestModelFactory(mock_request).construct_request_model() self.assertFalse(request_model.body) self.assertEqual(b"Raw body not available for multipart_form data, Silk is not showing file uploads.", request_model.raw_body) mock_request.body.assert_not_called() ================================================ FILE: project/tests/test_profile_dot.py ================================================ # std import cProfile import os import tempfile from contextlib import contextmanager from unittest.mock import MagicMock # 3rd party from django.test import TestCase from networkx.drawing.nx_pydot import read_dot # silk from silk.views.profile_dot import ( _create_dot, _create_profile, _temp_file_from_file_field, ) class ProfileDotViewTestCase(TestCase): @classmethod @contextmanager def _stats_file(cls): """ Context manager to create some arbitrary profiling stats in a temp file, returning the filename on enter, and removing the temp file on exit. """ try: with tempfile.NamedTemporaryFile(delete=False) as stats: pass cProfile.run('1+1', stats.name) yield stats.name finally: os.unlink(stats.name) @classmethod @contextmanager def _stats_data(cls): """ Context manager to create some arbitrary profiling stats in a temp file, returning the data on enter, and removing the temp file on exit. """ with cls._stats_file() as filename: with open(filename, 'rb') as f: yield f.read() @classmethod def _profile(cls): """Create some arbitrary profiling stats.""" with cls._stats_file() as filename: # create profile - we don't need to convert a django file field to a temp file # just use the filename of the temp file already created @contextmanager def dummy(_): yield filename return _create_profile(filename, dummy) @classmethod def _mock_file(cls, data): """ Get a mock object that looks like a file but returns data when read is called. """ i = [0] def read(n): if not i[0]: i[0] += 1 return data stream = MagicMock() stream.open = lambda: None stream.read = read return stream def test_create_dot(self): """ Verify that a dot file is correctly created from pstats data stored in a file field. """ with self._stats_file(): try: # create dot with tempfile.NamedTemporaryFile(delete=False) as dotfile: dot = _create_dot(self._profile(), 5) dotfile.write(dot.encode('utf-8')) # verify generated dot is valid G = read_dot(dotfile.name) self.assertGreater(len(G.nodes()), 0) finally: os.unlink(dotfile.name) def test_temp_file_from_file_field(self): """ Verify that data held in a file like object is copied to a temp file. """ dummy_data = b'dummy data' stream = self._mock_file(dummy_data) with _temp_file_from_file_field(stream) as filename: with open(filename, 'rb') as f: self.assertEqual(f.read(), dummy_data) # file should have been removed on exit self.assertFalse(os.path.exists(filename)) ================================================ FILE: project/tests/test_profile_parser.py ================================================ import contextlib import cProfile import io import re from django.test import TestCase from silk.utils.profile_parser import parse_profile class ProfileParserTestCase(TestCase): def test_profile_parser(self): """ Verify that the function parse_profile produces the expected output. """ with contextlib.closing(io.StringIO()) as stream: with contextlib.redirect_stdout(stream): cProfile.run('print()') stream.seek(0) actual = list(parse_profile(stream)) # Expected format for the profiling output on cPython implementations (PyPy differs) # actual = [ # ["ncalls", "tottime", "percall", "cumtime", "percall", "filename:lineno(function)"], # ["1", "0.000", "0.000", "0.000", "0.000", ":1()"], # ["1", "0.000", "0.000", "0.000", "0.000", "{built-in method builtins.exec}"], # ["1", "0.000", "0.000", "0.000", "0.000", "{built-in method builtins.print}"], # ["1", "0.000", "0.000", "0.000", "0.000", "{method 'disable' of '_lsprof.Profiler' objects}"], # ] exc_header = ["ncalls", "tottime", "percall", "cumtime", "percall", "filename:lineno(function)"] self.assertEqual(actual[0], exc_header) exc_number = re.compile(r"\d(.\d+)?") exc_module = re.compile(r"({method.*})|({built-in.*})|(<.+>:\d+\(<.+>\))") exc_row = [exc_number, exc_number, exc_number, exc_number, exc_number, exc_module] for row in actual[1:]: for text, expected_regex in zip(row, exc_row): self.assertRegex( text, expected_regex, msg="Expected something like {} but found {}" ) ================================================ FILE: project/tests/test_response_assumptions.py ================================================ from django.http import HttpResponse from django.test import TestCase class TestResponseAssumptions(TestCase): def test_headers_present_in_http_response(self): """Verify that HttpResponse has a headers or _headers attribute, which we use and Mock in our tests.""" django_response = HttpResponse() self.assertTrue(hasattr(django_response, '_headers') or hasattr(django_response, 'headers')) ================================================ FILE: project/tests/test_sensitive_data_in_request.py ================================================ import json from unittest.mock import Mock from django.test import TestCase from silk.config import SilkyConfig from silk.model_factory import RequestModelFactory HTTP_CONTENT_TYPE = 'content-type' CLEANSED = RequestModelFactory.CLEANSED_SUBSTITUTE DEFAULT_SENSITIVE_KEYS = {'username', 'api', 'token', 'key', 'secret', 'password', 'signature'} DEFAULT_HIDE_COOKIES = True class MaskCredentialsInFormsTest(TestCase): def tearDown(self): SilkyConfig().SILKY_SENSITIVE_KEYS = DEFAULT_SENSITIVE_KEYS def _mask(self, value): return RequestModelFactory(None)._mask_credentials(value) def test_mask_credentials_preserves_single_insensitive_values(self): body = "foo=public" expected = "foo=public" self.assertEqual(expected, self._mask(body)) def test_mask_credentials_masks_sensitive_values(self): body = "password=secret" expected = f"password={CLEANSED}" self.assertEqual(expected, self._mask(body)) def test_mask_credentials_masks_multiple_sensitive_values(self): body = "password=mypassword&secret=mysecret" expected = f"password={CLEANSED}&secret={CLEANSED}" self.assertEqual(expected, self._mask(body)) def test_mask_credentials_masks_sensitive_values_between_insensitive_values(self): body = "public1=foo&password=secret&public2=bar" expected = f"public1=foo&password={CLEANSED}&public2=bar" self.assertEqual(expected, self._mask(body)) def test_mask_credentials_preserves_insensitive_values_between_sensitive_values(self): body = "password=1&foo=public&secret=2" expected = f"password={CLEANSED}&foo=public&secret={CLEANSED}" self.assertEqual(expected, self._mask(body)) def test_mask_credentials_is_case_insensitive(self): body = "UsErNaMe=secret" expected = f"UsErNaMe={CLEANSED}" self.assertEqual(expected, self._mask(body)) def test_mask_credentials_handles_prefixes(self): body = "prefixed-username=secret" expected = f"prefixed-username={CLEANSED}" self.assertEqual(expected, self._mask(body)) def test_mask_credentials_handles_suffixes(self): body = "username-with-suffix=secret" expected = f"username-with-suffix={CLEANSED}" self.assertEqual(expected, self._mask(body)) def test_mask_credentials_handles_regex_characters(self): body = "password=secret++" expected = f"password={CLEANSED}" self.assertEqual(expected, self._mask(body)) def test_mask_credentials_handles_complex_cases(self): body = "foo=public&prefixed-uSeRname-with-suffix=secret&bar=public" expected = f"foo=public&prefixed-uSeRname-with-suffix={CLEANSED}&bar=public" self.assertEqual(expected, self._mask(body)) def test_mask_credentials_masks_sensitive_values_listed_in_settings(self): SilkyConfig().SILKY_SENSITIVE_KEYS = {"foo"} body = "foo=hidethis" expected = f"foo={CLEANSED}" self.assertEqual(expected, self._mask(body)) def test_sensitive_values_remain_unmasked_with_empty_settings(self): SilkyConfig().SILKY_SENSITIVE_KEYS = {} body = "foo=hidethis" expected = "foo=hidethis" self.assertEqual(expected, self._mask(body)) class MaskCredentialsInJsonTest(TestCase): def tearDown(self): SilkyConfig().SILKY_SENSITIVE_KEYS = DEFAULT_SENSITIVE_KEYS def _mask(self, value): return RequestModelFactory(None)._mask_credentials(json.dumps(value)) def test_mask_credentials_preserves_single_insensitive_values(self): self.assertIn("public", self._mask({"foo": "public"})) def test_mask_credentials_preserves_insensitive_values_in_presence_of_sensitive(self): self.assertIn("public", self._mask({"password": "secret", "foo": "public"})) def test_mask_credentials_masks_sensitive_values(self): self.assertNotIn("secret", self._mask({"password": "secret"})) def test_mask_credentials_masks_sensitive_values_in_presence_of_regular(self): self.assertNotIn("secret", self._mask({"foo": "public", "password": "secret"})) def test_mask_credentials_is_case_insensitive(self): self.assertNotIn("secret", self._mask({"UsErNaMe": "secret"})) def test_mask_credentials_handles_prefixes(self): self.assertNotIn("secret", self._mask({"prefixed-username": "secret"})) def test_mask_credentials_handles_suffixes(self): self.assertNotIn("secret", self._mask({"username-with-suffix": "secret"})) def test_mask_credentials_handles_complex_cases(self): self.assertNotIn("secret", self._mask({ "foo": "public", "prefixed-uSeRname-with-suffix": "secret" })) def test_mask_credentials_in_nested_data_structures(self): self.assertNotIn("secret", self._mask({ "foo": "public", "nested": { "prefixed-uSeRname-with-suffix": "secret", }, })) def test_mask_credentials_masks_sensitive_values_listed_in_settings(self): SilkyConfig().SILKY_SENSITIVE_KEYS = {"foo"} self.assertNotIn("hidethis", self._mask({"foo": "hidethis"})) def test_sensitive_values_remain_unmasked_with_empty_settings(self): SilkyConfig().SILKY_SENSITIVE_KEYS = {} self.assertIn("hidethis", self._mask({"foo": "hidethis"})) class TestEncodingForRequests(TestCase): """ Check that the RequestModelFactory masks sensitive data """ def tearDown(self): SilkyConfig().SILKY_SENSITIVE_KEYS = DEFAULT_SENSITIVE_KEYS SilkyConfig().SILKY_HIDE_COOKIES = DEFAULT_HIDE_COOKIES def test_password_in_body(self): mock_request = Mock() mock_request.headers = {HTTP_CONTENT_TYPE: 'text/plain'} mock_request.body = 'username=test_username&unmasked=testunmasked&password=testpassword' mock_request.get = mock_request.headers.get factory = RequestModelFactory(mock_request) body, raw_body = factory.body() self.assertIn('testunmasked', raw_body) self.assertNotIn('test_username', raw_body) self.assertNotIn('testpassword', raw_body) self.assertNotIn('test_username', body) self.assertNotIn('testpassword', body) def test_password_in_json(self): mock_request = Mock() mock_request.headers = {HTTP_CONTENT_TYPE: 'application/json; charset=UTF-8'} d = {'x': 'testunmasked', 'username': 'test_username', 'password': 'testpassword', 'prefixed-secret': 'testsecret'} mock_request.body = json.dumps(d) mock_request.get = mock_request.headers.get factory = RequestModelFactory(mock_request) body, raw_body = factory.body() self.assertIn('testunmasked', raw_body) self.assertNotIn('test_username', raw_body) self.assertNotIn('testpassword', raw_body) self.assertNotIn('testsecret', raw_body) self.assertNotIn('test_username', body) self.assertNotIn('testpassword', body) self.assertNotIn('testsecret', body) for datum in [json.loads(body), json.loads(raw_body)]: self.assertEqual(datum['username'], RequestModelFactory.CLEANSED_SUBSTITUTE) self.assertEqual(datum['password'], RequestModelFactory.CLEANSED_SUBSTITUTE) self.assertEqual(datum['prefixed-secret'], RequestModelFactory.CLEANSED_SUBSTITUTE) self.assertEqual(datum['x'], 'testunmasked') def test_password_in_batched_json(self): mock_request = Mock() mock_request.headers = {HTTP_CONTENT_TYPE: 'application/json; charset=UTF-8'} d = [ {'x': 'testunmasked', 'username': 'test_username', 'password': 'testpassword'}, {'x': 'testunmasked', 'username': 'test_username', 'password': 'testpassword'} ] mock_request.body = json.dumps(d) mock_request.get = mock_request.headers.get factory = RequestModelFactory(mock_request) body, raw_body = factory.body() self.assertIn('testunmasked', raw_body) self.assertNotIn('test_username', raw_body) self.assertNotIn('testpassword', raw_body) self.assertNotIn('test_username', body[0]) self.assertNotIn('testpassword', body[0]) self.assertNotIn('test_username', body[1]) self.assertNotIn('testpassword', body[1]) for data in [json.loads(body), json.loads(raw_body)]: for datum in data: self.assertEqual(datum['username'], RequestModelFactory.CLEANSED_SUBSTITUTE) self.assertEqual(datum['password'], RequestModelFactory.CLEANSED_SUBSTITUTE) self.assertEqual(datum['x'], 'testunmasked') def test_authorization_header(self): mock_request = Mock() mock_request.headers = {'authorization': 'secret'} mock_request.body = '' mock_request.get = mock_request.headers.get factory = RequestModelFactory(mock_request) headers = factory.encoded_headers() json_headers = json.loads(headers) self.assertIn('authorization', json_headers) self.assertEqual(json_headers['authorization'], RequestModelFactory.CLEANSED_SUBSTITUTE) def test_hide_cookies(self): SilkyConfig().SILKY_HIDE_COOKIES = True mock_request = Mock() mock_request.headers = {'Cookie': 'secret'} mock_request.body = '' mock_request.get = mock_request.headers.get factory = RequestModelFactory(mock_request) headers = factory.encoded_headers() json_headers = json.loads(headers) self.assertIn('cookie', json_headers) self.assertEqual(json_headers['cookie'], RequestModelFactory.CLEANSED_SUBSTITUTE) def test_no_hide_cookies(self): SilkyConfig().SILKY_HIDE_COOKIES = False mock_request = Mock() mock_request.headers = {'Cookie': 'Cookies!!!'} mock_request.body = '' mock_request.get = mock_request.headers.get factory = RequestModelFactory(mock_request) headers = factory.encoded_headers() json_headers = json.loads(headers) self.assertIn('cookie', json_headers) self.assertEqual(json_headers['cookie'], 'Cookies!!!') def test_hide_sensitive_headers(self): SilkyConfig().SILKY_SENSITIVE_KEYS = ["foo", "bar"] mock_request = Mock() mock_request.headers = {'FOO': 'secret', 'BAR': 'secret', 'BAZ': 'not-secret'} mock_request.body = '' mock_request.get = mock_request.headers.get factory = RequestModelFactory(mock_request) headers = factory.encoded_headers() json_headers = json.loads(headers) self.assertIn('foo', json_headers) self.assertIn('bar', json_headers) self.assertIn('baz', json_headers) self.assertEqual(json_headers['foo'], RequestModelFactory.CLEANSED_SUBSTITUTE) self.assertEqual(json_headers['bar'], RequestModelFactory.CLEANSED_SUBSTITUTE) self.assertEqual(json_headers['baz'], 'not-secret') ================================================ FILE: project/tests/test_silky_middleware.py ================================================ from unittest.mock import patch from django.test import TestCase, override_settings from django.urls import reverse from silk.config import SilkyConfig from silk.errors import SilkNotConfigured from silk.middleware import SilkyMiddleware, _should_intercept from silk.models import Request from .util import mock_data_collector def fake_get_response(): def fake_response(): return 'hello world' return fake_response class TestApplyDynamicMappings(TestCase): def test_dynamic_decorator(self): middleware = SilkyMiddleware(fake_get_response) SilkyConfig().SILKY_DYNAMIC_PROFILING = [ { 'module': 'tests.data.dynamic', 'function': 'foo' } ] middleware._apply_dynamic_mappings() from .data.dynamic import foo mock = mock_data_collector() with patch('silk.profiling.profiler.DataCollector', return_value=mock) as mock_DataCollector: foo() # Should be wrapped in a decorator self.assertTrue(mock_DataCollector.return_value.register_profile.call_count) def test_dynamic_context_manager(self): middleware = SilkyMiddleware(fake_get_response) SilkyConfig().SILKY_DYNAMIC_PROFILING = [ { 'module': 'tests.data.dynamic', 'function': 'foo', 'start_line': 1, 'end_line': 2, } ] middleware._apply_dynamic_mappings() from .data.dynamic import foo mock = mock_data_collector() with patch('silk.profiling.profiler.DataCollector', return_value=mock) as mock_DataCollector: foo() self.assertTrue(mock_DataCollector.return_value.register_profile.call_count) def test_invalid_dynamic_context_manager(self): middleware = SilkyMiddleware(fake_get_response) SilkyConfig().SILKY_DYNAMIC_PROFILING = [ { 'module': 'tests.data.dynamic', 'function': 'foo2', 'start_line': 1, 'end_line': 7, } ] self.assertRaises(IndexError, middleware._apply_dynamic_mappings) def test_invalid_dynamic_decorator_module(self): middleware = SilkyMiddleware(fake_get_response) SilkyConfig().SILKY_DYNAMIC_PROFILING = [ { 'module': 'tests.data.dfsdf', 'function': 'foo' } ] self.assertRaises(AttributeError, middleware._apply_dynamic_mappings) def test_invalid_dynamic_decorator_function_name(self): middleware = SilkyMiddleware(fake_get_response) SilkyConfig().SILKY_DYNAMIC_PROFILING = [ { 'module': 'tests.data.dynamic', 'function': 'bar' } ] self.assertRaises(AttributeError, middleware._apply_dynamic_mappings) def test_invalid_dynamic_mapping(self): middleware = SilkyMiddleware(fake_get_response) SilkyConfig().SILKY_DYNAMIC_PROFILING = [ { 'dfgdf': 'tests.data.dynamic', 'funcgdfgtion': 'bar' } ] self.assertRaises(KeyError, middleware._apply_dynamic_mappings) def test_no_mappings(self): middleware = SilkyMiddleware(fake_get_response) SilkyConfig().SILKY_DYNAMIC_PROFILING = [ ] middleware._apply_dynamic_mappings() # Just checking no crash def test_raise_if_authentication_is_enable_but_no_middlewares(self): SilkyConfig().SILKY_AUTHENTICATION = True with self.modify_settings(MIDDLEWARE={ 'remove': [ 'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', ], }): with self.assertRaisesMessage( SilkNotConfigured, "SILKY_AUTHENTICATION can not be enabled without Session, Authentication or Message Django's middlewares" ): SilkyMiddleware(fake_get_response) class TestShouldIntercept(TestCase): def test_should_intercept_non_silk_request(self): request = Request() request.path = '/myapp/foo' should_intercept = _should_intercept(request) self.assertTrue(should_intercept) def test_should_intercept_silk_request(self): request = Request() request.path = reverse('silk:summary') should_intercept = _should_intercept(request) self.assertFalse(should_intercept) @override_settings(ROOT_URLCONF='tests.urlconf_without_silk') def test_should_intercept_without_silk_urls(self): request = Request() request.path = '/login' _should_intercept(request) # Just checking no crash def test_should_intercept_ignore_paths(self): SilkyConfig().SILKY_IGNORE_PATHS = [ '/ignorethis' ] request = Request() request.path = '/ignorethis' should_intercept = _should_intercept(request) self.assertFalse(should_intercept) ================================================ FILE: project/tests/test_silky_profiler.py ================================================ from time import sleep from django.test import TestCase from silk.collector import DataCollector from silk.models import Request, _time_taken from silk.profiling.profiler import silk_profile from .test_lib.mock_suite import MockSuite class TestProfilerRequests(TestCase): def test_context_manager_no_request(self): DataCollector().configure() with silk_profile(name='test_profile'): sleep(0.1) self.assertFalse(DataCollector().profiles) def test_decorator_no_request(self): DataCollector().configure() @silk_profile() def func(): sleep(0.1) func() profile = list(DataCollector().profiles.values())[0] self.assertFalse(profile['request']) def test_context_manager_request(self): DataCollector().configure(Request.objects.create(path='/to/somewhere')) with silk_profile(name='test_profile'): sleep(0.1) profile = list(DataCollector().profiles.values())[0] self.assertEqual(DataCollector().request, profile['request']) def test_decorator_request(self): DataCollector().configure(Request.objects.create(path='/to/somewhere')) @silk_profile() def func(): sleep(0.1) func() profile = list(DataCollector().profiles.values())[0] self.assertEqual(DataCollector().request, profile['request']) class TestProfilertContextManager(TestCase): @classmethod def setUpClass(cls): super().setUpClass() r = Request.objects.create() DataCollector().configure(r) with silk_profile(name='test_profile'): sleep(0.1) def test_one_object(self): self.assertEqual(len(DataCollector().profiles), 1) def test_name(self): profile = list(DataCollector().profiles.values())[0] self.assertEqual(profile['name'], 'test_profile') def test_time_taken(self): profile = list(DataCollector().profiles.values())[0] time_taken = _time_taken(start_time=profile['start_time'], end_time=profile['end_time']) self.assertGreaterEqual(time_taken, 100) self.assertLess(time_taken, 110) class TestProfilerDecorator(TestCase): @classmethod def setUpClass(cls): super().setUpClass() DataCollector().configure(Request.objects.create()) @silk_profile() def func(): sleep(0.1) func() def test_one_object(self): self.assertEqual(len(DataCollector().profiles), 1) def test_name(self): profile = list(DataCollector().profiles.values())[0] self.assertEqual(profile['name'], 'func') def test_time_taken(self): profile = list(DataCollector().profiles.values())[0] time_taken = _time_taken(start_time=profile['start_time'], end_time=profile['end_time']) self.assertGreaterEqual(time_taken, 100) self.assertLess(time_taken, 115) class TestQueries(TestCase): def test_no_queries_before(self): DataCollector().configure(Request.objects.create()) with silk_profile(name='test_no_queries_before_profile'): mock_queries = MockSuite().mock_sql_queries(n=5, as_dict=True) DataCollector().register_query(*mock_queries) profile = list(DataCollector().profiles.values())[0] self.assertEqual(profile['name'], 'test_no_queries_before_profile') queries = profile['queries'] self.assertEqual(len(queries), 5) for query in DataCollector().queries: self.assertIn(query, queries) def test_queries_before(self): """test that any queries registered before profiling begins are ignored""" DataCollector().configure(Request.objects.create()) DataCollector().register_query(*MockSuite().mock_sql_queries(n=2, as_dict=True)) before = [x for x in DataCollector().queries] with silk_profile(name='test_no_queries_before_profile'): mock_queries = MockSuite().mock_sql_queries(n=5, as_dict=True) DataCollector().register_query(*mock_queries) profile = list(DataCollector().profiles.values())[0] self.assertEqual(profile['name'], 'test_no_queries_before_profile') queries = profile['queries'] self.assertEqual(len(queries), 5) for query in set(DataCollector().queries).difference(before): self.assertIn(query, queries) ================================================ FILE: project/tests/test_view_clear_db.py ================================================ from django.test import TestCase from silk import models from silk.config import SilkyConfig from silk.middleware import silky_reverse from .factories import RequestMinFactory class TestViewClearDB(TestCase): @classmethod def setUpClass(cls): super().setUpClass() SilkyConfig().SILKY_AUTHENTICATION = False SilkyConfig().SILKY_AUTHORISATION = False def test_clear_all(self): RequestMinFactory.create() self.assertEqual(models.Request.objects.count(), 1) response = self.client.post(silky_reverse("cleardb"), {"clear_all": "on"}) self.assertTrue(response.status_code == 200) self.assertEqual(models.Request.objects.count(), 0) class TestViewClearDBAndDeleteProfiles(TestCase): @classmethod def setUpClass(cls): super().setUpClass() SilkyConfig().SILKY_AUTHENTICATION = False SilkyConfig().SILKY_AUTHORISATION = False SilkyConfig().SILKY_DELETE_PROFILES = True def test_clear_all_and_delete_profiles(self): RequestMinFactory.create() self.assertEqual(models.Request.objects.count(), 1) response = self.client.post(silky_reverse("cleardb"), {"clear_all": "on"}) self.assertTrue(response.status_code == 200) self.assertEqual(models.Request.objects.count(), 0) ================================================ FILE: project/tests/test_view_profiling.py ================================================ from unittest.mock import Mock from django.test import TestCase from silk.middleware import silky_reverse from silk.views.profiling import ProfilingView from .test_lib.assertion import dict_contains from .test_lib.mock_suite import MockSuite class TestProfilingViewDefaults(TestCase): def test_func_names(self): profiles = [MockSuite().mock_profile() for _ in range(0, 3)] func_names = ProfilingView()._get_function_names() for p in profiles: self.assertIn(p.func_name, func_names) self.assertIn('', func_names) def test_show(self): self.assertIn(ProfilingView.default_show, ProfilingView.show) def test_order_by(self): self.assertIn(ProfilingView.defualt_order_by, ProfilingView.order_by) class TestProfilingViewGetObjects(TestCase): @classmethod def setUpClass(cls): super().setUpClass() cls.profiles = [MockSuite().mock_profile() for _ in range(0, 10)] def test_ordering(self): results = ProfilingView()._get_objects(order_by='Recent') self.assertSorted(results, 'start_time') def test_show(self): results = ProfilingView()._get_objects(show=5) self.assertEqual(5, len(results)) def test_func_name(self): func_name = 'a_func_name' self.profiles[1].func_name = func_name self.profiles[1].save() results = ProfilingView()._get_objects(func_name=func_name) for r in results: self.assertEqual(r.func_name, func_name) def assertSorted(self, objects, sort_field): for idx, r in enumerate(objects): try: nxt = objects[idx + 1] self.assertGreaterEqual(getattr(r, sort_field), getattr(nxt, sort_field)) except IndexError: pass class TestProfilingContext(TestCase): def test_default(self): request = Mock(spec_set=['GET', 'session']) request.GET = {} request.session = {} context = ProfilingView()._create_context(request) self.assertTrue(dict_contains({ 'show': ProfilingView.default_show, 'order_by': ProfilingView.defualt_order_by, 'options_show': ProfilingView.show, 'options_order_by': ProfilingView.order_by, 'options_func_names': ProfilingView()._get_function_names() }, context)) self.assertNotIn('path', context) self.assertIn('results', context) def test_get(self): request = Mock(spec_set=['GET', 'session']) request.session = {} show = 10 func_name = 'func_name' name = 'name' order_by = 'Time' request.GET = {'show': show, 'func_name': func_name, 'name': name, 'order_by': order_by} context = ProfilingView()._create_context(request) self.assertTrue(dict_contains({ 'show': show, 'order_by': order_by, 'func_name': func_name, 'name': name, 'options_show': ProfilingView.show, 'options_order_by': ProfilingView.order_by, 'options_func_names': ProfilingView()._get_function_names() }, context)) self.assertIn('results', context) def test_view_without_session_and_auth_middlewares(self): """ Filters are not present because there is no `session` to store them. """ with self.modify_settings(MIDDLEWARE={ 'remove': [ 'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', ], }): # test filters on GET show = 10 func_name = 'func_name' name = 'name' order_by = 'Time' response = self.client.get(silky_reverse('profiling'), { 'show': show, 'func_name': func_name, 'name': name, 'order_by': order_by }) context = response.context self.assertTrue(dict_contains({ 'show': show, 'order_by': order_by, 'func_name': func_name, 'name': name, 'options_show': ProfilingView.show, 'options_order_by': ProfilingView.order_by, 'options_func_names': ProfilingView()._get_function_names() }, context)) # test filters on POST response = self.client.post(silky_reverse('profiling'), { 'filter-overalltime-value': 100, 'filter-overalltime-typ': 'TimeSpentOnQueriesFilter', }) context = response.context self.assertTrue(dict_contains({ 'filters': { 'overalltime': {'typ': 'TimeSpentOnQueriesFilter', 'value': 100, 'str': 'DB Time >= 100'} }, }, context)) ================================================ FILE: project/tests/test_view_requests.py ================================================ import random import unittest from unittest.mock import Mock from django.test import TestCase from silk.middleware import silky_reverse from silk.views.requests import RequestsView from .test_lib.assertion import dict_contains from .test_lib.mock_suite import MockSuite class TestRootViewDefaults(TestCase): def test_path(self): requests = [MockSuite().mock_request() for _ in range(0, 3)] paths = RequestsView()._get_paths() for r in requests: self.assertIn(r.path, paths) def test_show(self): self.assertIn(RequestsView.default_show, RequestsView.show) def test_order_by(self): self.assertIn(RequestsView.default_order_by, RequestsView.order_by) class TestContext(TestCase): def test_default(self): request = Mock(spec_set=['GET', 'session']) request.session = {} request.GET = {} context = RequestsView()._create_context(request) self.assertTrue(dict_contains({ 'show': RequestsView.default_show, 'order_by': RequestsView.default_order_by, 'options_show': RequestsView.show, 'options_order_by': RequestsView().options_order_by, 'options_order_dir': RequestsView().options_order_dir, }, context)) self.assertQuerySetEqual(context['options_paths'], RequestsView()._get_paths()) self.assertNotIn('path', context) self.assertIn('results', context) def test_get(self): show = 10 path = '/path/to/somewhere/' order_by = 'path' response = self.client.get(silky_reverse('requests'), { 'show': show, 'path': path, 'order_by': order_by, }) context = response.context self.assertTrue(dict_contains({ 'show': show, 'order_by': order_by, 'path': path, 'options_show': RequestsView.show, 'options_order_by': RequestsView().options_order_by, 'options_order_dir': RequestsView().options_order_dir, }, context)) self.assertQuerySetEqual(context['options_paths'], RequestsView()._get_paths()) self.assertIn('results', context) def test_post(self): response = self.client.post(silky_reverse('requests'), { 'filter-overalltime-value': 100, 'filter-overalltime-typ': 'TimeSpentOnQueriesFilter', }) context = response.context self.assertTrue(dict_contains({ 'filters': { 'overalltime': {'typ': 'TimeSpentOnQueriesFilter', 'value': 100, 'str': 'DB Time >= 100'} }, }, context)) self.assertQuerySetEqual(context['options_paths'], RequestsView()._get_paths()) self.assertIn('results', context) def test_view_without_session_and_auth_middlewares(self): """ Filters are not present because there is no `session` to store them. """ with self.modify_settings(MIDDLEWARE={ 'remove': [ 'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', ], }): # test filters on GET show = 10 path = '/path/to/somewhere/' order_by = 'path' response = self.client.get(silky_reverse('requests'), { 'show': show, 'path': path, 'order_by': order_by, }) context = response.context self.assertTrue(dict_contains({ 'show': show, 'order_by': order_by, 'path': path, }, context)) # test filters on POST response = self.client.post(silky_reverse('requests'), { 'filter-overalltime-value': 100, 'filter-overalltime-typ': 'TimeSpentOnQueriesFilter', }) context = response.context self.assertTrue(dict_contains({ 'filters': { 'overalltime': {'typ': 'TimeSpentOnQueriesFilter', 'value': 100, 'str': 'DB Time >= 100'} }, }, context)) class TestGetObjects(TestCase): def assertSorted(self, objects, sort_field): for idx, r in enumerate(objects): try: nxt = objects[idx + 1] self.assertGreaterEqual(getattr(r, sort_field), getattr(nxt, sort_field)) except IndexError: pass @classmethod def setUpClass(cls): super().setUpClass() cls.requests = [MockSuite().mock_request() for _ in range(0, 50)] def test_defaults(self): objects = RequestsView()._get_objects() self.assertEqual(len(objects), 25) self.assertSorted(objects, 'start_time') def test_show(self): objects = RequestsView()._get_objects(show=10) self.assertEqual(len(objects), 10) def test_path(self): request = random.choice(self.requests) objects = RequestsView()._get_objects(path=request.path) for r in objects: self.assertEqual(r.path, request.path) @unittest.skip("Flaky") def test_time_spent_db_with_path(self): request = random.choice(self.requests) query_set = RequestsView()._get_objects(order_by='db_time', path=request.path) num_results = query_set.count() self.assertTrue(num_results) for result in query_set: self.assertEqual(result.path, request.path) class TestOrderingRequestView(TestCase): def assertSorted(self, objects, sort_field): for idx, r in enumerate(objects): try: nxt = objects[idx + 1] self.assertGreaterEqual(getattr(r, sort_field), getattr(nxt, sort_field)) except IndexError: pass def test_ordering(self): self.assertSorted(objects=RequestsView()._get_objects(order_by='start_time'), sort_field='start_time') self.assertSorted(objects=RequestsView()._get_objects(order_by='path'), sort_field='path') self.assertSorted(objects=RequestsView()._get_objects(order_by='num_sql_queries'), sort_field='num_sql_queries') self.assertSorted(objects=RequestsView()._get_objects(order_by='time_taken'), sort_field='time_taken') self.assertSorted(objects=RequestsView()._get_objects(order_by='db_time'), sort_field='db_time') ================================================ FILE: project/tests/test_view_sql_detail.py ================================================ import os import random from unittest.mock import patch from django.conf import settings from django.test import TestCase from silk.config import SilkyConfig from silk.middleware import silky_reverse from silk.views.sql_detail import SQLDetailView from .test_lib.mock_suite import MockSuite class TestViewSQLDetail(TestCase): @classmethod def setUpClass(cls): super().setUpClass() SilkyConfig().SILKY_AUTHENTICATION = False SilkyConfig().SILKY_AUTHORISATION = False def test_allowed_file_paths_nothing_specified(self): """by default we dont display any source, and it should return correctly""" request = MockSuite().mock_request() query = MockSuite().mock_sql_queries(request=request, n=1)[0] response = self.client.get(silky_reverse('request_sql_detail', kwargs={'sql_id': query.id, 'request_id': request.id})) self.assertTrue(response.status_code == 200) def test_allowed_file_paths_available_source(self): """if we request to view source that exists in the TB all should be fine""" request = MockSuite().mock_request() query = MockSuite().mock_sql_queries(request=request, n=1)[0] tb = query.traceback_ln_only _, files = SQLDetailView()._urlify(tb) file_path = random.choice(files) with open(file_path) as f: line_num = random.randint(0, len(f.read().split('\n'))) response = self.client.get(silky_reverse('request_sql_detail', kwargs={'sql_id': query.id, 'request_id': request.id}), data={ 'line_num': line_num, 'file_path': file_path }) self.assertTrue(response.status_code == 200) def test_allowed_file_paths_unavailable_source(self): """if we request to view source that is not in the traceback we should get a 403""" request = MockSuite().mock_request() query = MockSuite().mock_sql_queries(request=request, n=1)[0] file_path = settings.TEMP_DIR + '/blah' with open(file_path, 'w') as f: f.write('test') response = self.client.get(silky_reverse('request_sql_detail', kwargs={'sql_id': query.id, 'request_id': request.id}), data={ 'line_num': 0, 'file_path': file_path }) self.assertTrue(response.status_code == 403) def test_virtualenv_not_available_no_highlight(self): """if we don't have a virtualenv, there should be no code hightlighted""" request = MockSuite().mock_request() query = MockSuite().mock_sql_queries(request=request)[0] url = silky_reverse('request_sql_detail', kwargs={'sql_id': query.id, 'request_id': request.id}) with patch.dict(os.environ, {}, clear=True): self.assertIsNone(os.environ.get('VIRTUAL_ENV')) response = self.client.get(url) self.assertEqual(response.status_code, 200) self.assertContains(response, ' is-third-party') self.assertNotContains(response, ' not-third-party') def test_virtualenv_hightlight(self): """if we have a virtualenv, there should be code hightlighted""" request = MockSuite().mock_request() query = MockSuite().mock_sql_queries(request=request)[0] url = silky_reverse('request_sql_detail', kwargs={'sql_id': query.id, 'request_id': request.id}) with patch.dict(os.environ, {'VIRTUAL_ENV': '/some/virtualenv/that/doesnt/really/exist'}, clear=True): response = self.client.get(url) self.assertEqual(response.status_code, 200) self.assertContains(response, ' not-third-party') ================================================ FILE: project/tests/test_view_summary_view.py ================================================ from django.test import TestCase from silk.middleware import silky_reverse from silk.views.summary import SummaryView from .test_lib.assertion import dict_contains from .test_lib.mock_suite import MockSuite mock_suite = MockSuite() class TestSummaryView(TestCase): def test_longest_query_by_view(self): [mock_suite.mock_request() for _ in range(0, 10)] print([x.time_taken for x in SummaryView()._longest_query_by_view([])]) def test_view_without_session_and_auth_middlewares(self): """ Filters are not present because there is no `session` to store them. """ with self.modify_settings(MIDDLEWARE={ 'remove': [ 'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', ], }): # test filters on POST seconds = 3600 response = self.client.post(silky_reverse('summary'), { 'filter-seconds-value': seconds, 'filter-seconds-typ': 'SecondsFilter', }) context = response.context self.assertTrue(dict_contains({ 'filters': { 'seconds': {'typ': 'SecondsFilter', 'value': seconds, 'str': f'>{seconds} seconds ago'} } }, context)) ================================================ FILE: project/tests/urlconf_without_silk.py ================================================ from django.urls import include, path urlpatterns = [ path( 'example_app/', include('example_app.urls', namespace='example_app') ), ] ================================================ FILE: project/tests/util.py ================================================ import io from unittest.mock import Mock from django.core.files import File from django.core.files.storage import Storage from silk.models import Request def mock_data_collector(): mock = Mock() mock.queries = [] mock.local = Mock() r = Request() mock.local.request = r mock.request = r return mock def delete_all_models(model_class): """ A sqlite3-safe deletion function to avoid "django.db.utils.OperationalError: too many SQL variables" :param model_class: :return: """ while model_class.objects.count(): ids = model_class.objects.values_list('pk', flat=True)[:80] model_class.objects.filter(pk__in=ids).delete() class DictStorage(Storage): """Storage that stores files in a dictionary - for testing.""" def __init__(self): self.files = {} def open(self, name, mode="rb"): if name not in self.files: self.files[name] = b"" return File(io.BytesIO(self.files[name]), name=name) def get_valid_name(self, name): return name def exists(self, name): return name in self.files ================================================ FILE: project/wsgi.py ================================================ """WSGI config for django_silky project. It exposes the WSGI callable as a module-level variable named ``application``. For more information on this file, see https://docs.djangoproject.com/en/2.0/howto/deployment/wsgi/ """ import os os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") from django.core.wsgi import get_wsgi_application # noqa: E402 application = get_wsgi_application() ================================================ FILE: pyproject.toml ================================================ [tool.autopep8] ignore = "E501,E203,W503" in-place = true ================================================ FILE: pytest.ini ================================================ [pytest] addopts = --cov silk --cov-config .coveragerc --cov-append --cov-report term --cov-report=xml python_files = test.py tests.py test_*.py tests_*.py *_tests.py *_test.py DJANGO_SETTINGS_MODULE = project.settings ================================================ FILE: requirements.txt ================================================ coverage==7.13.0 factory-boy==3.3.3 freezegun==1.5.5 networkx==3.4.2 pillow==12.1.1 pydot==3.0.4 pygments==2.20.0 pytest-cov==7.0.0 pytest-django==4.11.1 ================================================ FILE: scss/components/cell.scss ================================================ .cell { display: inline-block; background-color: transparent; padding: 10px; margin-left: 10px; margin-top: 10px; border-radius: 4px; transition: background-color 0.15s ease, color 0.15s ease; div { margin: 2px; } .timestamp-div { margin-bottom: 15px; font-size: 13px; } .meta { font-size: 12px; color: #be5b43; .unit { font-size: 9px; font-weight: lighter !important; } } .method-div { font-weight: bold; font-size: 20px; } .path-div { font-size: 18px; margin-bottom: 15px; } } ================================================ FILE: scss/components/colors.scss ================================================ .very-good-font-color { color: #bac54b; } .good-font-color { color: #c3a948; } .ok-font-color { color: #c08245; } .bad-font-color { color: #be5b43; } .very-bad-font-color { color: #b9424f; } ================================================ FILE: scss/components/fonts.scss ================================================ /** * Fira Sans */ @font-face { font-family: FiraSans; src: url(../../fonts/fira/FiraSans-Regular.woff); font-weight: normal; } @font-face { font-family: FiraSans; src: url(../../fonts/fira/FiraSans-Medium.woff); font-weight: bold; } @font-face { font-family: FiraSans; src: url(../../fonts/fira/FiraSans-Bold.woff); font-weight: bolder; } @font-face { font-family: FiraSans; src: url(../../fonts/fira/FiraSans-Light.woff); font-weight: lighter; } @font-face { font-family: FiraSans; src: url(../../fonts/fira/FiraSans-RegularItalic.woff); font-weight: normal; font-style: italic; } @font-face { font-family: FiraSans; src: url(../../fonts/fira/FiraSans-MediumItalic.woff); font-weight: bold; font-style: italic; } @font-face { font-family: FiraSans; src: url(../../fonts/fira/FiraSans-BoldItalic.woff); font-weight: bolder; font-style: italic; } @font-face { font-family: FiraSans; src: url(../../fonts/fira/FiraSans-LightItalic.woff); font-weight: lighter; font-style: italic; } /** * Fantasque */ @font-face { font-family: Fantasque; src: url(../../fonts/fantasque/FantasqueSansMono-Regular.woff); font-weight: normal; } @font-face { font-family: Fantasque; src: url(../../fonts/fantasque/FantasqueSansMono-Bold.woff); font-weight: bold; } @font-face { font-family: Fantasque; src: url(../../fonts/fantasque/FantasqueSansMono-RegItalic.woff); font-weight: normal; font-style: italic; } @font-face { font-family: Fantasque; src: url(../../fonts/fantasque/FantasqueSansMono-BoldItalic.woff); font-weight: bold; font-style: italic; } ================================================ FILE: scss/components/heading.scss ================================================ .heading { width: 100%; background-color: transparent; height: 30px; display: table; font-weight: bold; margin-top: 20px; .inner-heading { display: table-cell; text-align: left; padding: 0; vertical-align: middle; } } ================================================ FILE: scss/components/numeric.scss ================================================ .numeric { font-weight: normal; } .unit { font-weight: normal; } .numeric .unit { font-size: 12px; } .numeric { font-size: 20px; } ================================================ FILE: scss/components/row.scss ================================================ .row-wrapper { display: table; margin: 2rem; width: 100%; width: -moz-available; width: -webkit-fill-available; width: fill-available; .row { display: table-row; transition: background-color 0.15s ease, color 0.15s ease; div { padding: 1rem; } .col { font-size: 20px; display: table-cell; } .timestamp-div { border-top-left-radius: 4px; border-bottom-left-radius: 4px; margin-bottom: 15px; font-size: 13px; } .meta { font-size: 12px; color: #be5b43; } .meta .unit { font-size: 9px; font-weight: lighter !important; } .method-div { font-weight: bold; font-size: 20px; } .path-div { font-size: 18px; margin-bottom: 15px; } .num-queries-div { border-top-right-radius: 4px; border-bottom-right-radius: 4px; } .spacing { .numeric { padding: 0 0.3rem; } .meta { padding: 0 0.3rem; } } } .row:hover { background-color: rgb(51, 51, 68); color: white; cursor: pointer; } } ================================================ FILE: scss/components/summary.scss ================================================ #error-div { margin: 10px; } #query-div { margin: auto; width: 960px; text-align: center; } #code { text-align: left; } .name-div { margin-top: 20px; margin-bottom: 15px; font-weight: bold; } .description { text-align: left; } ================================================ FILE: scss/pages/base.scss ================================================ body { font-family: FiraSans, "Helvetica Neue", Arial, sans-serif; background-color: #f3f3f3; margin: 0; font-weight: lighter; } pre { font-family: Fantasque; background-color: white !important; padding: 0.5em !important; margin: 0 !important; font-size: 14px; text-align: left; } code { font-family: Fantasque; background-color: white !important; padding: 0 !important; margin: 0 !important; font-size: 14px; } html { margin: 0; } #header { height: 50px; background-color: rgb(51, 51, 68); width: 100%; position: relative; padding: 0; } #header div { display: inline-block; } .menu { height: 50px; padding: 0; margin: 0; } .menu-item { height: 50px; padding-left: 10px; padding-right: 10px; margin: 0; margin-right: -4px; color: white; } .menu-item a { color: white !important; } #filter .menu-item { margin-right: 0px; } .selectable-menu-item { transition: background-color 0.15s ease, color 0.15s ease; } .selectable-menu-item:hover { background-color: #f3f3f3; cursor: pointer; color: black !important; } .selectable-menu-item:hover a { color: black !important; } .menu-item-selected { background-color: #f3f3f3; color: black !important; } .menu-item-selected a { color: black !important; } .menu-item-outer { display: table !important; height: 100%; width: 100%; } .menu-item-inner { display: table-cell !important; vertical-align: middle; width: 100%; } a:visited { color: black; } a { color: black; text-decoration: none; } #filter { height: 50px; position: absolute; right: 0; } .description { font-style: italic; font-size: 14px; margin-bottom: 5px; } ================================================ FILE: scss/pages/clear_db.scss ================================================ .wrapper { width: 100%; margin-bottom: 20px; } .inner { margin: auto; width: 960px; } .cleardb-form .cleardb-form-wrapper{ margin-bottom: 20px; } .cleardb-form label { display: block; margin-bottom: 8px; } .cleardb-form .btn { background: #333344; color: #fff; padding: 10px 20px; border-radius: 2px; cursor: pointer; box-shadow: none; font-size: 16px; line-height: 20px; border: 0; min-width: 150px; text-align: center; } .cleardb-form label :last-child { margin-bottom: 0; } .msg { margin-top: 20px; color: #bac54b; } ================================================ FILE: scss/pages/cprofile.scss ================================================ #query-info-div { margin-top: 15px; } #query-info-div .timestamp-div { font-size: 13px; } #pyprofile-div { display: block; margin: auto; width: 960px; } .pyprofile { text-align: left; } a { color: #45ADA8; } a:visited { color: #45ADA8; } a:hover { color: #547980; } a:active { color: #594F4F; } #graph-div { padding: 25px; background-color: white; display: block; margin-left: auto; margin-right: auto; margin-top: 25px; width: 960px; text-align: center; } #percent { width: 20px; } svg { display: block; } ================================================ FILE: scss/pages/detail_base.scss ================================================ #traceback { overflow: visible; } #time-div { text-align: center; margin-bottom: 30px; } #query-div { text-align: center; margin-bottom: 20px; } #query { text-align: left; margin: 0 auto; display: inline-block; } .line { width: 100%; display: inline-block; } .the-line { background-color: #c3c3c3; } pre { margin: 0; } ================================================ FILE: scss/pages/profile_detail.scss ================================================ #query-info-div { margin-top: 15px; } #query-info-div .timestamp-div { font-size: 13px; } #pyprofile-div { display: block; margin: auto; width: 960px; } .pyprofile { text-align: left; } a { color: #45ADA8; } a:visited { color: #45ADA8; } a:hover { color: #547980; } a:active { color: #594F4F; } #graph-div { padding: 25px; background-color: white; display: block; margin-left: auto; margin-right: auto; margin-top: 25px; width: 960px; text-align: center; } #percent { width: 20px; } svg { display: block; } ================================================ FILE: scss/pages/profiling.scss ================================================ .name-div { font-weight: bold; } .container { padding: 0 1em; } .description { } h2 { margin-bottom: 10px; } .pyprofile { overflow: scroll; max-height: 650px; } ================================================ FILE: scss/pages/raw.scss ================================================ pre { width: 100%; height: 100%; margin: 0; padding: 0; background-color: white !important; white-space: pre-wrap; /* css-3 */ white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ /*noinspection CssInvalidElement*/ white-space: -pre-wrap; /* Opera 4-6 */ white-space: -o-pre-wrap; /* Opera 7 */ word-wrap: break-word; /* Internet Explorer 5.5+ */ } code { font-family: Fantasque; background-color: white !important; width: 100% !important; height: auto; padding:0 !important; } body { margin: 0; padding: 0; } html { margin: 0; padding: 0; } ================================================ FILE: scss/pages/request.scss ================================================ pre { white-space: pre-wrap; /* css-3 */ white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ /*noinspection CssInvalidElement*/ white-space: -pre-wrap; /* Opera 4-6 */ white-space: -o-pre-wrap; /* Opera 7 */ word-wrap: break-word; /* Internet Explorer 5.5+ */ } #request-summary { } .cell { background-color: transparent; margin-top: 15px; } div.wrapper { width: 100%; } div.wrapper div#request-summary { margin: auto; text-align: center; width: 100%; } div.wrapper div#request-info { width: 960px; margin: auto auto 20px; } a { color: #45ADA8; } a:visited { color: #45ADA8; } a:hover { color: #547980; } a:active { color: #594F4F; } .headers { font-size: 12px; font-family: Fantasque; background-color: white; width: 100%; } .headers tr:hover { background-color: #f4f4f4; } .headers td { padding-bottom: 5px; padding-left: 5px; } .headers .key { font-weight: bold; } .headers .value { } ================================================ FILE: scss/pages/requests.scss ================================================ .container { padding: 0 1em; } .resizing-input input { background-color: white; padding-top: 2px; color: black; box-shadow: inset 0 0 3px black; } .resizing-input input::placeholder { color: #383838; opacity: 1; } .filter-section { line-height: 2.3; } ================================================ FILE: scss/pages/root_base.scss ================================================ .cell:hover { background-color: rgb(51, 51, 68); color: white; cursor: pointer; } .cell { text-align: center; } /* General styles for all menus */ .cbp-spmenu { background: rgb(51, 51, 68); position: fixed; } h3 { color: white; font-size: 1.9em; padding: 10px; margin: 0; font-weight: 300; background: rgb(51, 51, 68); } /*.cbp-spmenu div {*/ /*display: block;*/ /*color: #fff;*/ /*font-size: 1.1em;*/ /*font-weight: 300;*/ /*}*/ /* Orientation-dependent styles for the content of the menu */ .cbp-spmenu-vertical { width: 300px; height: 100%; top: 0; z-index: 1000; } /* Vertical menu that slides from the left or right */ .cbp-spmenu-right { right: -300px; } .cbp-spmenu-right.cbp-spmenu-open { right: 0px; } /* Push classes applied to the body */ .cbp-spmenu-push { overflow-x: hidden; position: relative; left: 0; } .cbp-spmenu-push-toleft { left: -300px; } /* Transitions */ .cbp-spmenu, .cbp-spmenu-push { -webkit-transition: all 0.3s ease; -moz-transition: all 0.3s ease; transition: all 0.3s ease; } /* Example media queries */ @media screen and (max-width: 55.1875em) { .cbp-spmenu-horizontal { font-size: 75%; height: 110px; } } .active-filter { font-size: 12px; color: white; } .active-filter td { padding: 0; margin: 0; } #cbp-spmenu-s2 button { color: white; background: none; border: none; } #cbp-spmenu-s2 button:hover { color: black; background: white; border: none; cursor: pointer; } #cbp-spmenu-s2 button:focus { outline: none; } #slider-label { margin-top: 5px; } #outer-slider { text-align: center; width: 80%; } #seconds { width: 200px; height: 50px; } #slider { width: 80%; margin: auto; margin-top: 10px; } input[type="number"], input[type="text"] { border: none; padding: 10px; width: 222px; } .add-button { font-size: 20px; font-weight: bold; } .add-button.disabled { color: darkgray !important; } .add-button.disabled:hover { background: transparent !important; cursor: default !important; } .button-div { display: inline-block; text-align: center; height: 50px; } .apply-div { display: inline-block; background-color: white; color: black; margin: 10px; padding: 10px; border-radius: 5px; } .apply-div:hover { cursor: pointer; } select { border-radius: 0; max-width: 100%; } @media screen and (-webkit-min-device-pixel-ratio: 0) { /*safari and chrome*/ select { //height: 30px; line-height: 30px; background: #f4f4f4; } } select::-moz-focus-inner { /*Remove button padding in FF*/ border: 0; padding: 0; } #cbp-spmenu-s2 p { margin-bottom: 10px; margin-left: 10px; margin-right: 10px; margin-top: 0; font-size: 12px; color: white; font-style: oblique; line-height: 16px;; } #filter-item { width: 24px; height: 20px; float: right; display: inline; background-image: url('../../filter.png'); background-size: contain; background-repeat: no-repeat; text-align: right; } #filter-item #num-filters { position: relative; top: 7px; left: 1px; } .resizing-input { display: inline-block; } .resizing-input input, .resizing-input span { font-size: 12px; font-family: FiraSans; white-space: pre; font-weight: 300; background-color: transparent; color: white; padding: 0; padding-left: 4px; padding-right: 4px; margin: 0; letter-spacing: 1px; } .resizing-input input:focus { outline: none; } .resizing-input input { } h4 { padding: 0; margin: 3px; margin-left: 10px; color: white; font-weight: lighter; font-size: 20px; } .filter-section { color: white; font-size: 12px; line-height: 2.3; padding: 10px; } .filter-section select { margin-bottom: 10px; } .filter-section input { padding: 4px; } ================================================ FILE: scss/pages/sql.scss ================================================ .right-aligned { text-align: right; } .center-aligned { text-align: center; } .left-aligned { text-align: left; } #table-pagination { text-align: center; margin: 20px; } table { border-spacing: 0; margin: auto; max-width: 920px; } tr { height: 20px; } tr.data-row:hover { background-color: rgb(51, 51, 68); color: white; cursor: pointer; } td { padding: 5px; padding-left: 20px; padding-right: 20px; } th { height: 40px; padding-left: 20px; padding-right: 20px; } #table-div { width: 100%; margin-top: 40px; } #table-pagination div { padding: 5px; } ================================================ FILE: scss/pages/sql_detail.scss ================================================ #traceback { width: 960px; margin: auto; } #traceback pre { margin-top: 15px !important; margin-bottom: 15px !important; } #traceback .not-third-party { font-weight: bold; } a:hover { color: #9dd0ff; } a:active { color: #594F4F; } code { background-color: transparent !important; } #query-div pre { background-color: transparent !important; } #query-div { padding-top: 15px; } #query-info-div div { padding-top: 5px; } #query-plan-div { text-align: left; width: 960px; margin: auto; } #plan-div code { margin: auto !important; display: inline-block; } #query-plan-head { padding-top: 5px; padding-bottom: 15px; text-align: center; margin: auto; } .file-path { font-size: 13px; } ================================================ FILE: scss/pages/summary.scss ================================================ .wrapper { width: 100%; margin-bottom: 20px; } .inner { margin: auto; width: 960px; } .summary-cell { display: inline-block; text-align: center; padding: 10px; margin-left: 10px; margin-top: 10px; } .summary-cell .desc { margin-top: 8px; font-size: 12px; } .no-data { font-size: 12px; font-style: oblique; margin-left: 10px; } h2 { margin-bottom: 0;; } #filters { margin-top: 10px; font-size: 12px; } #filters input, #filters span { color: black !important; font-weight: bold; } #filter-image { width: 20px; } #filter-cell { padding-left: 5px; } ================================================ FILE: setup.py ================================================ import os from setuptools import setup # allow setup.py to be run from any path os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) setup( name='django-silk', use_scm_version=True, packages=['silk'], include_package_data=True, license='MIT License', description='Silky smooth profiling for the Django Framework', long_description=open('README.md').read(), long_description_content_type='text/markdown', url='https://github.com/jazzband/django-silk', author='Michael Ford', author_email='mtford@gmail.com', classifiers=[ 'Development Status :: 5 - Production/Stable', 'Environment :: Web Environment', 'Framework :: Django', 'Framework :: Django :: 4.2', 'Framework :: Django :: 5.1', 'Framework :: Django :: 5.2', 'Framework :: Django :: 6.0', 'Intended Audience :: Developers', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: 3.13', 'Programming Language :: Python :: 3.14', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', ], install_requires=[ 'Django>=4.2', 'sqlparse', 'gprof2dot>=2017.09.19', ], extras_require={ 'formatting': ['autopep8'], }, python_requires='>=3.10', setup_requires=['setuptools_scm'], ) ================================================ FILE: silk/__init__.py ================================================ from importlib.metadata import version __version__ = version("django-silk") ================================================ FILE: silk/apps.py ================================================ from django.apps import AppConfig class SilkAppConfig(AppConfig): default_auto_field = "django.db.models.AutoField" name = "silk" ================================================ FILE: silk/auth.py ================================================ from functools import WRAPPER_ASSIGNMENTS, wraps from django.contrib.auth.decorators import login_required from django.core.exceptions import PermissionDenied from silk.config import SilkyConfig def login_possibly_required(function=None, **kwargs): if SilkyConfig().SILKY_AUTHENTICATION: return login_required(function, **kwargs) return function def permissions_possibly_required(function=None): if SilkyConfig().SILKY_AUTHORISATION: actual_decorator = user_passes_test( SilkyConfig().SILKY_PERMISSIONS ) if function: return actual_decorator(function) return actual_decorator return function def user_passes_test(test_func): def decorator(view_func): @wraps(view_func, assigned=WRAPPER_ASSIGNMENTS) def _wrapped_view(request, *args, **kwargs): if test_func(request.user): return view_func(request, *args, **kwargs) else: raise PermissionDenied return _wrapped_view return decorator ================================================ FILE: silk/code_generation/__init__.py ================================================ __author__ = 'mtford' ================================================ FILE: silk/code_generation/curl.py ================================================ import json from urllib.parse import urlencode from django.template import Context, Template curl_template = """\ curl {% if method %}-X {{ method }}{% endif %} {% if content_type %}-H 'content-type: {{ content_type }}'{% endif %} {% if modifier %}{{ modifier }} {% endif %}{% if body %}'{{ body }}'{% endif %} {{ url }}{% if query_params %}{{ query_params }}{% endif %} {% if extra %}{{ extra }}{% endif %} """ def _curl_process_params(body, content_type, query_params): extra = None if query_params: try: query_params = urlencode( [(k, v.encode('utf8')) for k, v in query_params.items()] ) except TypeError: pass query_params = '?' + str(query_params) if 'json' in content_type or 'javascript' in content_type: if isinstance(body, dict): body = json.dumps(body) modifier = '-d' # See http://curl.haxx.se/docs/manpage.html#-F # for multipart vs x-www-form-urlencoded # x-www-form-urlencoded is same way as browser, # multipart is RFC 2388 which allows file uploads. elif 'multipart' in content_type or 'x-www-form-urlencoded' in content_type: try: body = ' '.join([f'{k}={v}' for k, v in body.items()]) except AttributeError: modifier = '-d' else: content_type = None modifier = '-F' elif body: body = str(body) modifier = '-d' else: modifier = None content_type = None # TODO: Clean up. return modifier, body, query_params, content_type, extra def curl_cmd(url, method=None, query_params=None, body=None, content_type=None): if not content_type: content_type = 'text/plain' modifier, body, query_params, content_type, extra = _curl_process_params( body, content_type, query_params, ) t = Template(curl_template) context = { 'url': url, 'method': method, 'query_params': query_params, 'body': body, 'modifier': modifier, 'content_type': content_type, 'extra': extra, } return t.render(Context(context, autoescape=False)).replace('\n', ' ') ================================================ FILE: silk/code_generation/django_test_client.py ================================================ from urllib.parse import urlencode try: import autopep8 except ImportError: autopep8 = None from django.template import Context, Template from silk.profiling.dynamic import is_str_typ template = """\ from django.test import Client c = Client() response = c.{{ lower_case_method }}(path='{{ path }}'{% if data or content_type %},{% else %}){% endif %}{% if data %} data={{ data }}{% endif %}{% if data and content_type %},{% elif data %}){% endif %}{% if content_type %} content_type='{{ content_type }}'){% endif %} """ def _encode_query_params(query_params): try: query_params = urlencode(query_params) except TypeError: pass return '?' + query_params def gen(path, method=None, query_params=None, data=None, content_type=None): # generates python code representing a call via django client. # useful for use in testing method = method.lower() t = Template(template) context = { 'path': path, 'lower_case_method': method, 'content_type': content_type, } if method == 'get': context['data'] = query_params else: if query_params: query_params = _encode_query_params(query_params) path += query_params if is_str_typ(data): data = "'%s'" % data context['data'] = data context['query_params'] = query_params code = t.render(Context(context, autoescape=False)) if autopep8: # autopep8 is not a hard requirement, so we check if it's available # if autopep8 is available, we use it to format the code and do things # like remove long lines and improve readability code = autopep8.fix_code( code, options=autopep8.parse_args(['--aggressive', '']), ) return code ================================================ FILE: silk/collector.py ================================================ import cProfile import logging import marshal import pstats import re import unicodedata from io import StringIO from threading import local from silk import models from silk.config import SilkyConfig from silk.errors import SilkInternalInconsistency, SilkNotConfigured from silk.models import _time_taken from silk.singleton import Singleton TYP_SILK_QUERIES = 'silk_queries' TYP_PROFILES = 'profiles' TYP_QUERIES = 'queries' Logger = logging.getLogger('silk.collector') def raise_middleware_error(): raise RuntimeError( 'Silk middleware has not been installed correctly. Ordering must ensure that Silk middleware can ' 'execute process_request and process_response. If an earlier middleware returns from either of ' 'these methods, Silk will not have the chance to inspect the request/response objects.') class DataCollector(metaclass=Singleton): """ Provides the ability to save all models at the end of the request. We cannot save during the request due to the possibility of atomic blocks and hence must collect data and perform the save at the end. """ def __init__(self): super().__init__() self.local = local() self._configure() def ensure_middleware_installed(self): if not hasattr(self.local, 'temp_identifier'): raise_middleware_error() @property def request(self): return getattr(self.local, 'request', None) def get_identifier(self): self.ensure_middleware_installed() self.local.temp_identifier += 1 return self.local.temp_identifier @request.setter def request(self, value): self.local.request = value def _configure(self): self.local.objects = {} self.local.temp_identifier = 0 self.stop_python_profiler() self.local.pythonprofiler = None @property def objects(self): return getattr(self.local, 'objects', None) @property def queries(self): return self._get_objects(TYP_QUERIES) @property def silk_queries(self): return self._get_objects(TYP_SILK_QUERIES) def _get_objects(self, typ): objects = self.objects if objects is None: self._raise_not_configured( 'Attempt to access %s without initialisation.' % typ ) if typ not in objects: objects[typ] = {} return objects[typ] @property def profiles(self): return self._get_objects(TYP_PROFILES) def configure(self, request=None, should_profile=True): self.request = request self._configure() if should_profile: self.local.pythonprofiler = cProfile.Profile() try: self.local.pythonprofiler.enable() except ValueError as e: # pragma: no cover # Deal with cProfile not being allowed to run concurrently # https://github.com/jazzband/django-silk/issues/682 Logger.error('Could not enable python profiler, %s' % str(e), exc_info=True) self.local.pythonprofiler = None def clear(self): self.request = None self._configure() def _raise_not_configured(self, err): raise SilkNotConfigured(err + ' Is the middleware installed correctly?') def register_objects(self, typ, *args): self.ensure_middleware_installed() for arg in args: ident = self.get_identifier() objects = self.objects if objects is None: # This can happen if the SilkyMiddleware.process_request is not # called for whatever reason. Perhaps if another piece of # middleware is not playing ball. self._raise_not_configured( 'Attempt to register object of type %s without initialisation. ' ) if typ not in objects: self.objects[typ] = {} self.objects[typ][ident] = arg def register_query(self, *args): self.register_objects(TYP_QUERIES, *args) def register_profile(self, *args): self.register_objects(TYP_PROFILES, *args) def _record_meta_profiling(self): if SilkyConfig().SILKY_META: num_queries = len(self.silk_queries) query_time = sum(_time_taken(x['start_time'], x['end_time']) for _, x in self.silk_queries.items()) self.request.meta_num_queries = num_queries self.request.meta_time_spent_queries = query_time def stop_python_profiler(self): if getattr(self.local, 'pythonprofiler', None): self.local.pythonprofiler.disable() def finalise(self): if getattr(self.local, 'pythonprofiler', None): s = StringIO() ps = pstats.Stats(self.local.pythonprofiler, stream=s).sort_stats('cumulative') ps.print_stats() profile_text = s.getvalue() profile_text = "\n".join( profile_text.split("\n")[0:256]) # don't record too much because it can overflow the field storage size self.request.pyprofile = profile_text if SilkyConfig().SILKY_PYTHON_PROFILER_BINARY: proposed_file_name = self._get_proposed_file_name() file_name = self.request.prof_file.storage.get_available_name(proposed_file_name) with self.request.prof_file.storage.open(file_name, 'w+b') as f: marshal.dump(ps.stats, f) self.request.prof_file = f.name sql_queries = [] for identifier, query in self.queries.items(): query['identifier'] = identifier sql_query = models.SQLQuery(**query) sql_queries += [sql_query] models.SQLQuery.objects.bulk_create(sql_queries) sql_queries = models.SQLQuery.objects.filter(request=self.request) for sql_query in sql_queries.all(): query = self.queries.get(sql_query.identifier) if query: query['model'] = sql_query for profile in self.profiles.values(): profile_query_models = [] if TYP_QUERIES in profile: profile_queries = profile[TYP_QUERIES] del profile[TYP_QUERIES] for query_temp_id in profile_queries: try: query = self.queries[query_temp_id] try: profile_query_models.append(query['model']) except KeyError: raise SilkInternalInconsistency( 'Profile references a query dictionary that has not ' 'been converted into a Django model. This should ' 'never happen, please file a bug report' ) except KeyError: raise SilkInternalInconsistency( 'Profile references a query temp_id that does not exist. ' 'This should never happen, please file a bug report' ) profile = models.Profile.objects.create(**profile) if profile_query_models: profile.queries.set(profile_query_models) self._record_meta_profiling() def register_silk_query(self, *args): self.register_objects(TYP_SILK_QUERIES, *args) def _get_proposed_file_name(self) -> str: """Retrieve the profile file name to be proposed to the storage""" if SilkyConfig().SILKY_PYTHON_PROFILER_EXTENDED_FILE_NAME: slugified_path = slugify_path(self.request.path) return f"{slugified_path}_{str(self.request.id)}.prof" return f"{str(self.request.id)}.prof" def slugify_path(request_path: str) -> str: """ Convert any characters not included in [a-zA-Z0-9_]) with a single underscore. Convert to lowercase. Also strip any leading and trailing char that are not in the accepted list Inspired from django slugify """ request_path = str(request_path) request_path = ( unicodedata.normalize("NFKD", request_path) .encode("ascii", "ignore") .decode("ascii") ) request_path = request_path.lower()[:50] return re.sub(r'\W+', '_', request_path).strip('_') ================================================ FILE: silk/config.py ================================================ from copy import copy from silk.singleton import Singleton def default_permissions(user): if user: return user.is_staff return False class SilkyConfig(metaclass=Singleton): defaults = { 'SILKY_DYNAMIC_PROFILING': [], 'SILKY_IGNORE_PATHS': [], 'SILKY_HIDE_COOKIES': True, 'SILKY_IGNORE_QUERIES': [], 'SILKY_META': False, 'SILKY_AUTHENTICATION': False, 'SILKY_AUTHORISATION': False, 'SILKY_PERMISSIONS': default_permissions, 'SILKY_MAX_RECORDED_REQUESTS': 10**4, 'SILKY_MAX_RECORDED_REQUESTS_CHECK_PERCENT': 10, 'SILKY_MAX_REQUEST_BODY_SIZE': -1, 'SILKY_MAX_RESPONSE_BODY_SIZE': -1, 'SILKY_INTERCEPT_PERCENT': 100, 'SILKY_INTERCEPT_FUNC': None, 'SILKY_PYTHON_PROFILER': False, 'SILKY_PYTHON_PROFILER_FUNC': None, 'SILKY_STORAGE_CLASS': 'silk.storage.ProfilerResultStorage', 'SILKY_PYTHON_PROFILER_EXTENDED_FILE_NAME': False, 'SILKY_MIDDLEWARE_CLASS': 'silk.middleware.SilkyMiddleware', 'SILKY_JSON_ENSURE_ASCII': True, 'SILKY_ANALYZE_QUERIES': False, 'SILKY_EXPLAIN_FLAGS': None, 'SILKY_SENSITIVE_KEYS': {'username', 'api', 'token', 'key', 'secret', 'password', 'signature'}, 'SILKY_DELETE_PROFILES': False } def _setup(self): from django.conf import settings options = {option: getattr(settings, option) for option in dir(settings) if option.startswith('SILKY')} self.attrs = copy(self.defaults) self.attrs['SILKY_PYTHON_PROFILER_RESULT_PATH'] = settings.MEDIA_ROOT self.attrs.update(options) def __init__(self): super().__init__() self._setup() def __getattr__(self, item): return self.attrs.get(item, None) def __setattribute__(self, key, value): self.attrs[key] = value ================================================ FILE: silk/errors.py ================================================ class SilkError(Exception): pass class SilkNotConfigured(SilkError): pass class SilkInternalInconsistency(SilkError): pass ================================================ FILE: silk/management/__init__.py ================================================ ================================================ FILE: silk/management/commands/__init__.py ================================================ ================================================ FILE: silk/management/commands/silk_clear_request_log.py ================================================ from django.core.management.base import BaseCommand import silk.models from silk.utils.data_deletion import delete_model class Command(BaseCommand): help = "Clears silk's log of requests." def handle(self, *args, **options): # Django takes a long time to traverse foreign key relations, # so delete in the order that makes it easy. delete_model(silk.models.Profile) delete_model(silk.models.SQLQuery) delete_model(silk.models.Response) delete_model(silk.models.Request) ================================================ FILE: silk/management/commands/silk_request_garbage_collect.py ================================================ from django.core.management.base import BaseCommand import silk.models from silk.config import SilkyConfig class Command(BaseCommand): help = "Triggers silk's request garbage collect." def add_arguments(self, parser): parser.add_argument( "-m", "--max-requests", default=SilkyConfig().SILKY_MAX_RECORDED_REQUESTS, type=int, help="Maximum number of requests to keep after garbage collection.", ) def handle(self, *args, **options): if "max_requests" in options: max_requests = options["max_requests"] SilkyConfig().SILKY_MAX_RECORDED_REQUESTS = max_requests if options["verbosity"] >= 2: max_requests = SilkyConfig().SILKY_MAX_RECORDED_REQUESTS request_count = silk.models.Request.objects.count() self.stdout.write( f"Keeping up to {max_requests} of {request_count} requests." ) silk.models.Request.garbage_collect(force=True) ================================================ FILE: silk/middleware.py ================================================ import logging import random from django.conf import settings from django.db import DatabaseError, router, transaction from django.db.models.sql.compiler import SQLCompiler from django.urls import NoReverseMatch, reverse from django.utils import timezone from django.utils.translation import gettext_lazy as _ from silk import models from silk.collector import DataCollector from silk.config import SilkyConfig from silk.errors import SilkNotConfigured from silk.model_factory import RequestModelFactory, ResponseModelFactory from silk.profiling import dynamic from silk.profiling.profiler import silk_meta_profiler from silk.sql import execute_sql Logger = logging.getLogger('silk.middleware') def silky_reverse(name, *args, **kwargs): try: r = reverse('silk:%s' % name, *args, **kwargs) except NoReverseMatch: # In case user forgets to set namespace, but also fixes Django 1.5 tests on Travis # Hopefully if user has forgotten to add namespace there are no clashes with their own # view names but I don't think there is really anything can do about this. r = reverse(name, *args, **kwargs) return r def get_fpath(): return silky_reverse('summary') config = SilkyConfig() AUTH_AND_SESSION_MIDDLEWARES = [ 'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', ] def _should_intercept(request): """we want to avoid recording any requests/sql queries etc that belong to Silky""" # Check custom intercept logic. if config.SILKY_INTERCEPT_FUNC: if not config.SILKY_INTERCEPT_FUNC(request): return False # don't trap every request elif config.SILKY_INTERCEPT_PERCENT < 100: if random.random() > config.SILKY_INTERCEPT_PERCENT / 100.0: return False try: silky = request.path.startswith(get_fpath()) except NoReverseMatch: silky = False ignored = request.path in config.SILKY_IGNORE_PATHS return not (silky or ignored) class TestMiddleware: def process_response(self, request, response): return response def process_request(self, request): return class SilkyMiddleware: def __init__(self, get_response): if config.SILKY_AUTHENTICATION and not ( set(AUTH_AND_SESSION_MIDDLEWARES) & set(settings.MIDDLEWARE) ): raise SilkNotConfigured( _("SILKY_AUTHENTICATION can not be enabled without Session, " "Authentication or Message Django's middlewares") ) self.get_response = get_response def __call__(self, request): self.process_request(request) # To be able to persist filters when Session and Authentication # middlewares are not present. # Unlike session (which stores in DB) it won't persist filters # after refresh the page. request.silk_filters = {} response = self.get_response(request) response = self.process_response(request, response) return response def _apply_dynamic_mappings(self): dynamic_profile_configs = config.SILKY_DYNAMIC_PROFILING for conf in dynamic_profile_configs: module = conf.get('module') function = conf.get('function') start_line = conf.get('start_line') end_line = conf.get('end_line') name = conf.get('name') if module and function: if start_line and end_line: # Dynamic context manager dynamic.inject_context_manager_func(module=module, func=function, start_line=start_line, end_line=end_line, name=name) else: # Dynamic decorator dynamic.profile_function_or_method(module=module, func=function, name=name) else: raise KeyError('Invalid dynamic mapping %s' % conf) @silk_meta_profiler() def process_request(self, request): DataCollector().clear() if not _should_intercept(request): return Logger.debug('process_request') request.silk_is_intercepted = True self._apply_dynamic_mappings() if not hasattr(SQLCompiler, '_execute_sql'): SQLCompiler._execute_sql = SQLCompiler.execute_sql SQLCompiler.execute_sql = execute_sql silky_config = SilkyConfig() should_profile = silky_config.SILKY_PYTHON_PROFILER if silky_config.SILKY_PYTHON_PROFILER_FUNC: should_profile = silky_config.SILKY_PYTHON_PROFILER_FUNC(request) request_model = RequestModelFactory(request).construct_request_model() DataCollector().configure(request_model, should_profile=should_profile) def _process_response(self, request, response): # Use a context manager instead of a decorator so db_for_write is evaluated at runtime, # which is important for dynamic database configurations (e.g., multitenancy). with transaction.atomic(using=router.db_for_write(models.SQLQuery)): Logger.debug('Process response') with silk_meta_profiler(): collector = DataCollector() collector.stop_python_profiler() silk_request = collector.request if silk_request: ResponseModelFactory(response).construct_response_model() silk_request.end_time = timezone.now() collector.finalise() else: Logger.error( 'No request model was available when processing response. ' 'Did something go wrong in process_request/process_view?' '\n' + str(request) + '\n\n' + str(response) ) # Need to save the data outside the silk_meta_profiler # Otherwise the meta time collected in the context manager # is not taken in account if silk_request: silk_request.save() Logger.debug('Process response done.') def process_response(self, request, response): max_attempts = 2 attempts = 1 if getattr(request, 'silk_is_intercepted', False): while attempts <= max_attempts: if attempts > 1: Logger.debug('Retrying _process_response; attempt %s' % attempts) try: self._process_response(request, response) break except (AttributeError, DatabaseError): if attempts >= max_attempts: Logger.warning('Exhausted _process_response attempts; not processing request') break attempts += 1 return response ================================================ FILE: silk/migrations/0001_initial.py ================================================ import uuid import django.utils.timezone from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ] operations = [ migrations.CreateModel( name='Profile', fields=[ ('id', models.AutoField(serialize=False, primary_key=True, verbose_name='ID', auto_created=True)), ('name', models.CharField(max_length=300, blank=True, default='')), ('start_time', models.DateTimeField(default=django.utils.timezone.now)), ('end_time', models.DateTimeField(blank=True, null=True)), ('time_taken', models.FloatField(blank=True, null=True)), ('file_path', models.CharField(max_length=300, blank=True, default='')), ('line_num', models.IntegerField(blank=True, null=True)), ('end_line_num', models.IntegerField(blank=True, null=True)), ('func_name', models.CharField(max_length=300, blank=True, default='')), ('exception_raised', models.BooleanField(default=False)), ('dynamic', models.BooleanField(default=False)), ], options={ 'abstract': False, }, ), migrations.CreateModel( name='Request', fields=[ ('id', models.CharField(max_length=36, primary_key=True, default=uuid.uuid1, serialize=False)), ('path', models.CharField(db_index=True, max_length=190)), ('query_params', models.TextField(blank=True, default='')), ('raw_body', models.TextField(blank=True, default='')), ('body', models.TextField(blank=True, default='')), ('method', models.CharField(max_length=10)), ('start_time', models.DateTimeField(db_index=True, default=django.utils.timezone.now)), ('view_name', models.CharField(db_index=True, blank=True, default='', max_length=190, null=True)), ('end_time', models.DateTimeField(blank=True, null=True)), ('time_taken', models.FloatField(blank=True, null=True)), ('encoded_headers', models.TextField(blank=True, default='')), ('meta_time', models.FloatField(blank=True, null=True)), ('meta_num_queries', models.IntegerField(blank=True, null=True)), ('meta_time_spent_queries', models.FloatField(blank=True, null=True)), ('pyprofile', models.TextField(blank=True, default='')), ('num_sql_queries', models.IntegerField(default=0)), ], ), migrations.CreateModel( name='Response', fields=[ ('id', models.CharField(max_length=36, primary_key=True, default=uuid.uuid1, serialize=False)), ('status_code', models.IntegerField()), ('raw_body', models.TextField(blank=True, default='')), ('body', models.TextField(blank=True, default='')), ('encoded_headers', models.TextField(blank=True, default='')), ('request', models.OneToOneField(to='silk.Request', related_name='response', on_delete=models.CASCADE)), ], ), migrations.CreateModel( name='SQLQuery', fields=[ ('id', models.AutoField(serialize=False, primary_key=True, verbose_name='ID', auto_created=True)), ('query', models.TextField()), ('start_time', models.DateTimeField(default=django.utils.timezone.now, blank=True, null=True)), ('end_time', models.DateTimeField(blank=True, null=True)), ('time_taken', models.FloatField(blank=True, null=True)), ('traceback', models.TextField()), ('request', models.ForeignKey(to='silk.Request', blank=True, null=True, related_name='queries', on_delete=models.CASCADE)), ], ), migrations.AddField( model_name='profile', name='queries', field=models.ManyToManyField(to='silk.SQLQuery', db_index=True, related_name='profiles'), ), migrations.AddField( model_name='profile', name='request', field=models.ForeignKey(to='silk.Request', blank=True, null=True, on_delete=models.CASCADE), ), ] ================================================ FILE: silk/migrations/0002_auto_update_uuid4_id_field.py ================================================ import uuid from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('silk', '0001_initial'), ] operations = [ migrations.AlterField( model_name='request', name='id', field=models.CharField(default=uuid.uuid4, max_length=36, serialize=False, primary_key=True), ), migrations.AlterField( model_name='response', name='id', field=models.CharField(default=uuid.uuid4, max_length=36, serialize=False, primary_key=True), ), ] ================================================ FILE: silk/migrations/0003_request_prof_file.py ================================================ # Generated by Django 1.9.7 on 2016-07-08 18:23 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('silk', '0002_auto_update_uuid4_id_field'), ] operations = [ migrations.AddField( model_name='request', name='prof_file', field=models.FileField(null=True, upload_to=''), ), ] ================================================ FILE: silk/migrations/0004_request_prof_file_storage.py ================================================ # Generated by Django 1.10.4 on 2016-12-06 00:23 from django.db import migrations, models import silk.models class Migration(migrations.Migration): dependencies = [ ('silk', '0003_request_prof_file'), ] operations = [ migrations.AlterField( model_name='request', name='prof_file', field=models.FileField(null=True, storage=silk.models.silk_storage, upload_to=''), ), ] ================================================ FILE: silk/migrations/0005_increase_request_prof_file_length.py ================================================ # Generated by Django 1.11.3 on 2017-07-31 23:40 from django.db import migrations, models import silk.models class Migration(migrations.Migration): dependencies = [ ('silk', '0004_request_prof_file_storage'), ] operations = [ migrations.AlterField( model_name='request', name='prof_file', field=models.FileField(max_length=300, null=True, storage=silk.models.silk_storage, upload_to=''), ), ] ================================================ FILE: silk/migrations/0006_fix_request_prof_file_blank.py ================================================ # Generated by Django 2.0 on 2017-12-28 14:21 from django.db import migrations, models import silk.storage class Migration(migrations.Migration): dependencies = [ ('silk', '0005_increase_request_prof_file_length'), ] operations = [ migrations.AlterField( model_name='request', name='prof_file', field=models.FileField(blank=True, max_length=300, storage=silk.storage.ProfilerResultStorage(), upload_to=''), ), ] ================================================ FILE: silk/migrations/0007_sqlquery_identifier.py ================================================ # Generated by Django 2.2.6 on 2019-10-26 12:57 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('silk', '0006_fix_request_prof_file_blank'), ] operations = [ migrations.AddField( model_name='sqlquery', name='identifier', field=models.IntegerField(default=-1), ), ] ================================================ FILE: silk/migrations/0008_sqlquery_analysis.py ================================================ # Generated by Django 2.2.17 on 2020-11-26 13:17 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('silk', '0007_sqlquery_identifier'), ] operations = [ migrations.AddField( model_name='sqlquery', name='analysis', field=models.TextField(blank=True, null=True), ), ] ================================================ FILE: silk/migrations/__init__.py ================================================ ================================================ FILE: silk/model_factory.py ================================================ import base64 import json import logging import re import sys import traceback from uuid import UUID from django.core.exceptions import RequestDataTooBig from django.urls import Resolver404, resolve from silk import models from silk.collector import DataCollector from silk.config import SilkyConfig Logger = logging.getLogger('silk.model_factory') content_types_json = ['application/json', 'application/x-javascript', 'text/javascript', 'text/x-javascript', 'text/x-json'] multipart_form = 'multipart/form-data' content_type_form = [multipart_form, 'application/x-www-form-urlencoded'] content_type_html = ['text/html'] content_type_css = ['text/css'] class DefaultEncoder(json.JSONEncoder): def default(self, o): if isinstance(o, UUID): return str(o) def _parse_content_type(content_type): """best efforts on pulling out the content type and encoding from content-type header""" char_set = None if content_type.strip(): splt = content_type.split(';') content_type = splt[0] try: raw_char_set = splt[1].strip() key, char_set = raw_char_set.split('=') if key != 'charset': char_set = None except (IndexError, ValueError): pass return content_type, char_set class RequestModelFactory: """Produce Request models from Django request objects""" # String to replace on masking CLEANSED_SUBSTITUTE = '********************' def __init__(self, request): super().__init__() self.request = request def content_type(self): content_type = self.request.headers.get('content-type', '') return _parse_content_type(content_type) def encoded_headers(self): """ From Django docs (https://docs.djangoproject.com/en/2.0/ref/request-response/#httprequest-objects): """ sensitive_headers = set(map(str.lower, SilkyConfig().SILKY_SENSITIVE_KEYS)) sensitive_headers.add('authorization') if SilkyConfig().SILKY_HIDE_COOKIES: sensitive_headers.add('cookie') headers = {} for k, v in self.request.headers.items(): k = k.lower() if k in sensitive_headers: v = RequestModelFactory.CLEANSED_SUBSTITUTE headers[k] = v return json.dumps(headers, cls=DefaultEncoder, ensure_ascii=SilkyConfig().SILKY_JSON_ENSURE_ASCII) def _mask_credentials(self, body): """ Mask credentials of potentially sensitive info before saving to db. """ sensitive_keys = SilkyConfig().SILKY_SENSITIVE_KEYS key_string = '|'.join(sensitive_keys) def replace_pattern_values(obj): pattern = re.compile(key_string, re.I) if isinstance(obj, dict): for key in obj.keys(): if key_string and pattern.search(key): obj[key] = RequestModelFactory.CLEANSED_SUBSTITUTE else: obj[key] = replace_pattern_values(obj[key]) elif isinstance(obj, list): for index, item in enumerate(obj): obj[index] = replace_pattern_values(item) else: if key_string and pattern.search(str(obj)): return RequestModelFactory.CLEANSED_SUBSTITUTE return obj try: json_body = json.loads(body) except Exception as e: if key_string: pattern = re.compile(fr'(({key_string})[^=]*)=(.*?)(&|$)', re.M | re.I) try: body = re.sub(pattern, f'\\1={RequestModelFactory.CLEANSED_SUBSTITUTE}\\4', body) except Exception: Logger.debug(f'{str(e)}') else: body = json.dumps(replace_pattern_values(json_body), ensure_ascii=SilkyConfig().SILKY_JSON_ENSURE_ASCII) return body def _body(self, raw_body, content_type): """ Encode body as JSON if possible so can be used as a dictionary in generation of curl/django test client code """ body = '' if content_type in content_type_form: body = self.request.POST body = json.dumps(dict(body), sort_keys=True, indent=4 , ensure_ascii=SilkyConfig().SILKY_JSON_ENSURE_ASCII) elif content_type in content_types_json: try: body = json.dumps(json.loads(raw_body), sort_keys=True, indent=4 , ensure_ascii=SilkyConfig().SILKY_JSON_ENSURE_ASCII) except Exception: body = raw_body return body def body(self): content_type, char_set = self.content_type() if content_type == multipart_form: raw_body = b"Raw body not available for multipart_form data, Silk is not showing file uploads." body = '' return body, raw_body try: raw_body = self.request.body except RequestDataTooBig: raw_body = b"Raw body exceeds DATA_UPLOAD_MAX_MEMORY_SIZE, Silk is not showing file uploads." body = self.request.POST.copy() for k, v in self.request.FILES.items(): body.appendlist(k, v) return body, raw_body if char_set: try: raw_body = raw_body.decode(char_set) except AttributeError: pass except LookupError: # If no encoding exists, default to UTF-8 try: raw_body = raw_body.decode('UTF-8') except AttributeError: pass except UnicodeDecodeError: raw_body = '' except Exception as e: Logger.error( 'Unable to decode request body using char_set %s due to error: %s. Will ignore. Stacktrace:' % (char_set, e) ) traceback.print_exc() else: # Default to an attempt at UTF-8 decoding. try: raw_body = raw_body.decode('UTF-8') except AttributeError: pass except UnicodeDecodeError: raw_body = '' max_size = SilkyConfig().SILKY_MAX_REQUEST_BODY_SIZE body = '' if raw_body: if max_size > -1: Logger.debug('A max request size is set so checking size') size = sys.getsizeof(raw_body, default=None) request_identifier = self.request.path if not size: Logger.error( 'No way in which to get size of request body for %s, will ignore it', request_identifier ) elif size <= max_size: Logger.debug( 'Request %s has body of size %d which is less than %d so will save the body' % (request_identifier, size, max_size) ) body = self._body(raw_body, content_type) else: Logger.debug( 'Request %s has body of size %d which is greater than %d, therefore ignoring' % (request_identifier, size, max_size) ) raw_body = None else: Logger.debug('No maximum request body size is set, continuing.') body = self._body(raw_body, content_type) body = self._mask_credentials(body) raw_body = self._mask_credentials(raw_body) return body, raw_body def query_params(self): query_params = self.request.GET encoded_query_params = '' if query_params: query_params_dict = dict(zip(query_params.keys(), query_params.values())) encoded_query_params = json.dumps(query_params_dict, ensure_ascii=SilkyConfig().SILKY_JSON_ENSURE_ASCII) return encoded_query_params def view_name(self): try: resolved = resolve(self.request.path_info) except Resolver404: return None return resolved.view_name def construct_request_model(self): body, raw_body = self.body() query_params = self.query_params() path = self.request.path view_name = self.view_name() request_model = models.Request.objects.create( path=path, encoded_headers=self.encoded_headers(), method=self.request.method, query_params=query_params, view_name=view_name, body=body) # Text fields are encoded as UTF-8 in Django and hence will try to coerce # anything to we pass to UTF-8. Some stuff like binary will fail. try: request_model.raw_body = raw_body except UnicodeDecodeError: Logger.debug('NYI: Binary request bodies') # TODO Logger.debug('Created new request model with pk %s' % request_model.pk) return request_model class ResponseModelFactory: """given a response object, craft the silk response model""" def __init__(self, response): super().__init__() self.response = response self.request = DataCollector().request def body(self): body = '' content_type, char_set = _parse_content_type(self.response.get('content-type', '')) content = getattr(self.response, 'content', '') if content: max_body_size = SilkyConfig().SILKY_MAX_RESPONSE_BODY_SIZE if max_body_size > -1: Logger.debug('Max size of response body defined so checking') size = sys.getsizeof(content, None) if not size: Logger.error('Could not get size of response body. Ignoring') content = '' else: if size > max_body_size: content = '' Logger.debug( 'Size of %d for %s is bigger than %d so ignoring response body' % (size, self.request.path, max_body_size) ) else: Logger.debug( 'Size of %d for %s is less than %d so saving response body' % (size, self.request.path, max_body_size) ) if content and content_type in content_types_json: # TODO: Perhaps theres a way to format the JSON without parsing it? try: body = json.dumps(json.loads(content), sort_keys=True, indent=4 , ensure_ascii=SilkyConfig().SILKY_JSON_ENSURE_ASCII) except (TypeError, ValueError): Logger.warning( 'Response to request with pk %s has content type %s but was unable to parse it' % (self.request.pk, content_type) ) return body, content def construct_response_model(self): assert self.request, 'Cant construct a response model if there is no request model' Logger.debug( 'Creating response model for request model with pk %s' % self.request.pk ) b, content = self.body() headers = {} for k, v in self.response.headers.items(): try: header, val = v except ValueError: header, val = k, v finally: headers[header] = val silky_response = models.Response( request_id=self.request.id, status_code=self.response.status_code, encoded_headers=json.dumps(headers, ensure_ascii=SilkyConfig().SILKY_JSON_ENSURE_ASCII), body=b ) try: raw_body = base64.b64encode(content) except TypeError: raw_body = base64.b64encode(content.encode('utf-8')) silky_response.raw_body = raw_body.decode('ascii') silky_response.save() return silky_response ================================================ FILE: silk/models.py ================================================ import base64 import json import random import re from uuid import uuid4 import sqlparse from django.conf import settings from django.core.files.storage import storages from django.core.files.storage.handler import InvalidStorageError from django.db import models, router, transaction from django.db.models import ( BooleanField, CharField, DateTimeField, FileField, FloatField, ForeignKey, IntegerField, ManyToManyField, OneToOneField, Sum, TextField, ) from django.utils import timezone from django.utils.safestring import mark_safe from silk.config import SilkyConfig from silk.utils.profile_parser import parse_profile try: silk_storage = storages['SILKY_STORAGE'] except InvalidStorageError: from django.utils.module_loading import import_string storage_class = SilkyConfig().SILKY_STORAGE_CLASS or settings.DEFAULT_FILE_STORAGE silk_storage = import_string(storage_class)() # Seperated out so can use in tests w/o models def _time_taken(start_time, end_time): d = end_time - start_time return d.seconds * 1000 + d.microseconds / 1000 def time_taken(self): return _time_taken(self.start_time, self.end_time) class CaseInsensitiveDictionary(dict): def __getitem__(self, key): return super().__getitem__(key.lower()) def __setitem__(self, key, value): super().__setitem__(key.lower(), value) def update(self, other=None, **kwargs): for k, v in other.items(): self[k] = v for k, v in kwargs.items(): self[k] = v def __init__(self, d): super().__init__() for k, v in d.items(): self[k] = v class Request(models.Model): id = CharField(max_length=36, default=uuid4, primary_key=True) path = CharField(max_length=190, db_index=True) query_params = TextField(blank=True, default='') raw_body = TextField(blank=True, default='') body = TextField(blank=True, default='') method = CharField(max_length=10) start_time = DateTimeField(default=timezone.now, db_index=True) view_name = CharField( max_length=190, db_index=True, blank=True, default='', null=True ) end_time = DateTimeField(null=True, blank=True) time_taken = FloatField(blank=True, null=True) # milliseconds encoded_headers = TextField(blank=True, default='') # stores json meta_time = FloatField(null=True, blank=True) meta_num_queries = IntegerField(null=True, blank=True) meta_time_spent_queries = FloatField(null=True, blank=True) pyprofile = TextField(blank=True, default='') prof_file = FileField(max_length=300, blank=True, storage=silk_storage) # Useful method to create shortened copies of strings without losing start and end context # Used to ensure path and view_name don't exceed 190 characters def _shorten(self, string): return f'{string[:94]}...{string[len(string) - 93:]}' @property def total_meta_time(self): return (self.meta_time or 0) + (self.meta_time_spent_queries or 0) @property def profile_table(self): for n, columns in enumerate(parse_profile(self.pyprofile)): location = columns[-1] if n and '{' not in location and '<' not in location: r = re.compile(r'(?P.*\.py)\:(?P[0-9]+).*') m = r.search(location) group = m.groupdict() src = group['src'] num = group['num'] name = 'c%d' % n fmt = '{location}' rep = fmt.format(**dict(group, **locals())) yield columns[:-1] + [mark_safe(rep)] else: yield columns # defined in atomic transaction within SQLQuery save()/delete() as well # as in bulk_create of SQLQueryManager # TODO: This is probably a bad way to do this, .count() will prob do? num_sql_queries = IntegerField(default=0) # TODO replace with count() @property def time_spent_on_sql_queries(self): """" Calculate the total time spent in milliseconds on SQL queries using Django aggregates. """ result = SQLQuery.objects.filter(request=self).aggregate( total_time=Sum('time_taken', output_field=FloatField()) ) return result['total_time'] or 0.0 @property def headers(self): if self.encoded_headers: raw = json.loads(self.encoded_headers) else: raw = {} return CaseInsensitiveDictionary(raw) @property def content_type(self): return self.headers.get('content-type', None) @classmethod def garbage_collect(cls, force=False): """ Remove Request/Responses when we are at the SILKY_MAX_RECORDED_REQUESTS limit Note that multiple in-flight requests may call this at once causing a double collection """ check_percent = SilkyConfig().SILKY_MAX_RECORDED_REQUESTS_CHECK_PERCENT check_percent /= 100.0 if check_percent < random.random() and not force: return target_count = SilkyConfig().SILKY_MAX_RECORDED_REQUESTS # Since garbage collection is probabilistic, the target count should # be lowered to account for requests before the next garbage collection if check_percent != 0: target_count -= int(1 / check_percent) # Make sure we can delete everything if needed by settings if target_count <= 0: cls.objects.all().delete() return try: time_cutoff = cls.objects.order_by( '-start_time' ).values_list( 'start_time', flat=True )[target_count] except IndexError: return cls.objects.filter(start_time__lte=time_cutoff).delete() def save(self, *args, **kwargs): # sometimes django requests return the body as 'None' if self.raw_body is None: self.raw_body = '' if self.body is None: self.body = '' if self.end_time and self.start_time: interval = self.end_time - self.start_time self.time_taken = interval.total_seconds() * 1000 # We can't save if either path or view_name exceed 190 characters if self.path and len(self.path) > 190: self.path = self._shorten(self.path) if self.view_name and len(self.view_name) > 190: self.view_name = self._shorten(self.view_name) super().save(*args, **kwargs) Request.garbage_collect(force=False) class Response(models.Model): id = CharField(max_length=36, default=uuid4, primary_key=True) request = OneToOneField( Request, related_name='response', db_index=True, on_delete=models.CASCADE, ) status_code = IntegerField() raw_body = TextField(blank=True, default='') body = TextField(blank=True, default='') encoded_headers = TextField(blank=True, default='') @property def content_type(self): return self.headers.get('content-type', None) @property def headers(self): if self.encoded_headers: raw = json.loads(self.encoded_headers) else: raw = {} return CaseInsensitiveDictionary(raw) @property def raw_body_decoded(self): return base64.b64decode(self.raw_body) # TODO rewrite docstring class SQLQueryManager(models.Manager): def bulk_create(self, *args, **kwargs): """ensure that num_sql_queries remains consistent. Bulk create does not call the model save() method and hence we must add this logic here too""" with transaction.atomic(using=router.db_for_write(SQLQuery)): if len(args): objs = args[0] else: objs = kwargs.get('objs') for obj in objs: obj.prepare_save() return super().bulk_create(*args, **kwargs) class SQLQuery(models.Model): query = TextField() start_time = DateTimeField(null=True, blank=True, default=timezone.now) end_time = DateTimeField(null=True, blank=True) time_taken = FloatField(blank=True, null=True) # milliseconds identifier = IntegerField(default=-1) request = ForeignKey( Request, related_name='queries', null=True, blank=True, db_index=True, on_delete=models.CASCADE, ) traceback = TextField() analysis = TextField(null=True, blank=True) objects = SQLQueryManager() # TODO docstring @property def traceback_ln_only(self): return '\n'.join(self.traceback.split('\n')[::2]) @property def formatted_query(self): return sqlparse.format(self.query, reindent=True, keyword_case='upper') @property def num_joins(self): parsed_query = sqlparse.parse(self.query) count = 0 for statement in parsed_query: count += sum(map(lambda t: t.match(sqlparse.tokens.Keyword, r'\.*join\.*', regex=True), statement.flatten())) return count @property def first_keywords(self): parsed_query = sqlparse.parse(self.query) keywords = [] for statement in parsed_query[0].tokens: if not statement.is_keyword: break keywords.append(statement.value) return ' '.join(keywords) @property def tables_involved(self): """ A really another rudimentary way to work out tables involved in a query. TODO: Can probably parse the SQL using sqlparse etc and pull out table info that way? """ components = [x.strip() for x in self.query.split()] tables = [] for idx, component in enumerate(components): # TODO: If django uses aliases on column names they will be falsely # identified as tables... if ( component.lower() == "from" or component.lower() == "join" or component.lower() == "as" or component.lower() == "update" ): try: _next = components[idx + 1] if not _next.startswith('('): # Subquery stripped = _next.strip().strip(',') if stripped: tables.append(stripped) except IndexError: # Reach the end pass return tables def prepare_save(self): if self.end_time and self.start_time: interval = self.end_time - self.start_time self.time_taken = interval.total_seconds() * 1000 if not self.pk: if self.request: self.request.num_sql_queries += 1 self.request.save(update_fields=['num_sql_queries']) def save(self, *args, **kwargs): with transaction.atomic(using=router.db_for_write(self)): self.prepare_save() super().save(*args, **kwargs) def delete(self, *args, **kwargs): with transaction.atomic(using=router.db_for_write(self)): self.request.num_sql_queries -= 1 self.request.save() super().delete(*args, **kwargs) class BaseProfile(models.Model): name = CharField(max_length=300, blank=True, default='') start_time = DateTimeField(default=timezone.now) end_time = DateTimeField(null=True, blank=True) request = ForeignKey( Request, null=True, blank=True, db_index=True, on_delete=models.CASCADE, ) time_taken = FloatField(blank=True, null=True) # milliseconds class Meta: abstract = True def save(self, *args, **kwargs): if self.end_time and self.start_time: interval = self.end_time - self.start_time self.time_taken = interval.total_seconds() * 1000 super().save(*args, **kwargs) class Profile(BaseProfile): file_path = CharField(max_length=300, blank=True, default='') line_num = IntegerField(null=True, blank=True) end_line_num = IntegerField(null=True, blank=True) func_name = CharField(max_length=300, blank=True, default='') exception_raised = BooleanField(default=False) queries = ManyToManyField(SQLQuery, related_name='profiles', db_index=True) dynamic = BooleanField(default=False) @property def is_function_profile(self): return self.func_name is not None @property def is_context_profile(self): return self.func_name is None @property def time_spent_on_sql_queries(self): """ Calculate the total time spent in milliseconds on SQL queries using Django aggregates. """ result = self.queries.aggregate( total_time=Sum('time_taken', output_field=FloatField()) ) return result['total_time'] or 0.0 ================================================ FILE: silk/profiling/__init__.py ================================================ __author__ = 'mtford' ================================================ FILE: silk/profiling/dynamic.py ================================================ import inspect import logging import re import sys from silk.profiling.profiler import silk_profile Logger = logging.getLogger('silk.profiling.dynamic') def _get_module(module_name): """ Given a module name in form 'path.to.module' return module object for 'module'. """ if '.' in module_name: splt = module_name.split('.') imp = '.'.join(splt[:-1]) frm = splt[-1] module = __import__(imp, globals(), locals(), [frm], 0) module = getattr(module, frm) else: module = __import__(module_name, globals(), locals(), [], 0) return module def _get_func(module, func_name): """ Given a module and a function name, return the function. func_name can be of the forms: - 'foo': return a function - 'Class.foo': return a method """ cls_name = None cls = None if '.' in func_name: cls_name, func_name = func_name.split('.') if cls_name: cls = getattr(module, cls_name) func = getattr(cls, func_name) else: func = getattr(module, func_name) return cls, func def profile_function_or_method(module, func, name=None): """ Programmatically apply a decorator to a function in a given module [+ class] @param module: module object or module name in form 'path.to.module' @param func: function object or function name in form 'foo' or 'Class.method' """ if isinstance(module, str): module = _get_module(module) decorator = silk_profile(name, _dynamic=True) func_name = func cls, func = _get_func(module, func_name) wrapped_target = decorator(func) if cls: setattr(cls, func_name.split('.')[-1], wrapped_target) else: setattr(module, func_name, wrapped_target) def _get_parent_module(module): parent = sys.modules splt = module.__name__.split('.') if len(splt) > 1: for module_name in splt[:-1]: try: parent = getattr(parent, module_name) except AttributeError: parent = parent[module_name] return parent def _get_context_manager_source(end_line, file_path, name, start_line): inject_code = "with silk_profile('%s', _dynamic=True):\n" % name code = 'from silk.profiling.profiler import silk_profile\n' with open(file_path) as f: ws = '' for i, line in enumerate(f): if i == start_line: # Use the same amount of whitespace as the line currently occupying x = re.search(r"^(\s+).*$", line) try: ws = x.groups()[0] except IndexError: ws = '' code += ws + inject_code code += ws + ' ' + line elif start_line < i <= end_line: code += ws + ' ' + line else: code += line return code def _get_ws(txt): """ Return whitespace at the beginning of a string """ m = re.search(r"^(\s+).*$", txt) try: fws = m.groups()[0] except AttributeError: fws = '' return fws def _get_source_lines(func): source = inspect.getsourcelines(func)[0] fws = _get_ws(source[0]) for i in range(0, len(source)): source[i] = source[i].replace(fws, '', 1) return source def _new_func_from_source(source, func): """ Create new function defined in source but maintain context from func @param func: The function whose global + local context we will use @param source: Python source code containing def statement """ src_str = ''.join(source) frames = inspect.getouterframes(inspect.currentframe()) calling_frame = frames[2][0] context = {} # My initial instinct was: exec src_str in func.func_globals.items(), calling_frame.f_locals # however this seems to break the function closure so caveat here is that we create a new # function with the locals merged into the globals. # # Possible consequences I can think of: # - If a global exists that already has the same name as the local, it will be overwritten in # in the context of this function. This shouldnt matter though as the global should have already # been hidden by the new name? # # This functionality should be considered experimental as no idea what other consequences there # could be. # # relevant: http://stackoverflow.com/questions/2749655/why-are-closures-broken-within-exec globals = func.__globals__ locals = calling_frame.f_locals combined = globals.copy() combined.update(locals) Logger.debug('New src_str:\n %s' % src_str) exec(src_str, combined, context) return context[func.__name__] def _inject_context_manager_func(func, start_line, end_line, name): """ injects a context manager into the given function e.g given: x = 5 def foo(): print x print '1' print '2' print '3' inject_context_manager_func(foo, 0, 2, 'cm') foo will now have the definition: def foo(): with silk_profile('cm'): print x print '1' print '2' print '3' closures, globals & locals are honoured @param func: object of type or type @param start_line: line at which to inject 'with' statement. line num. is relative to the func, not the module. @param end_line: line at which to exit the context @param name: name of the profiler """ source = _get_source_lines(func) start_line += 1 end_line += 1 ws = _get_ws(source[start_line]) for i in range(start_line, end_line): try: source[i] = ' ' + source[i] except IndexError: raise IndexError('Function %s does not have line %d' % (func.__name__, i)) source.insert(start_line, ws + "from silk.profiling.profiler import silk_profile\n") source.insert(start_line + 1, ws + "with silk_profile('%s', _dynamic=True):\n" % name) return _new_func_from_source(source, func) def is_str_typ(o): return isinstance(o, str) def inject_context_manager_func(module, func, start_line, end_line, name): if is_str_typ(module): module = _get_module(module) cls = None if is_str_typ(func): func_name = func cls, func = _get_func(module, func_name) else: func_name = func.__name__ new_func = _inject_context_manager_func(func, start_line, end_line, name) if cls: setattr(cls, func_name, new_func) else: setattr(module, func_name, new_func) ================================================ FILE: silk/profiling/profiler.py ================================================ import inspect import logging import time import traceback from functools import wraps from django.apps import apps from django.conf import settings from django.utils import timezone from silk.collector import DataCollector from silk.config import SilkyConfig from silk.models import _time_taken logger = logging.getLogger('silk.profiling.profiler') # noinspection PyPep8Naming class silk_meta_profiler: """Used in the profiling of Silk itself.""" def __init__(self): super().__init__() self.start_time = None @property def _should_meta_profile(self): return SilkyConfig().SILKY_META def __enter__(self): if self._should_meta_profile: self.start_time = timezone.now() def __exit__(self, exc_type, exc_val, exc_tb): if self._should_meta_profile: end_time = timezone.now() exception_raised = exc_type is not None if exception_raised: logger.error('Exception when performing meta profiling, dumping trace below') traceback.print_exception(exc_type, exc_val, exc_tb) request = getattr(DataCollector().local, 'request', None) if request: curr = request.meta_time or 0 request.meta_time = curr + _time_taken(self.start_time, end_time) def __call__(self, target): if self._should_meta_profile: def wrapped_target(*args, **kwargs): request = DataCollector().request if request: start_time = timezone.now() result = target(*args, **kwargs) end_time = timezone.now() curr = request.meta_time or 0 request.meta_time = curr + _time_taken(start_time, end_time) else: result = target(*args, **kwargs) return result return wrapped_target return target # noinspection PyPep8Naming class silk_profile: def __init__(self, name=None, _dynamic=False): super().__init__() self.name = name self.profile = None self._queries_before = None self._queries_after = None self._dynamic = _dynamic def _query_identifiers_from_collector(self): return [x for x in DataCollector().queries] def _start_queries(self): """record queries that have been executed before profiling began""" self._queries_before = self._query_identifiers_from_collector() def _end_queries(self): """record queries that have been executed after profiling has finished""" self._queries_after = self._query_identifiers_from_collector() def __enter__(self): if self._silk_installed() and self._should_profile(): with silk_meta_profiler(): self._start_queries() if not self.name: raise ValueError('silk_profile used as a context manager must have a name') frame = inspect.currentframe() frames = inspect.getouterframes(frame) outer_frame = frames[1] path = outer_frame[1] line_num = outer_frame[2] request = DataCollector().request self.profile = { 'name': self.name, 'file_path': path, 'line_num': line_num, 'dynamic': self._dynamic, 'request': request, 'start_time': timezone.now(), } else: logger.warning('Cannot execute silk_profile as silk is not installed correctly.') def _finalise_queries(self): collector = DataCollector() self._end_queries() assert self.profile, 'no profile was created' diff = set(self._queries_after).difference(set(self._queries_before)) self.profile['queries'] = diff collector.register_profile(self.profile) # noinspection PyUnusedLocal def __exit__(self, exc_type, exc_val, exc_tb): if self._silk_installed() and self._should_profile(): with silk_meta_profiler(): exception_raised = exc_type is not None self.profile['exception_raised'] = exception_raised self.profile['end_time'] = timezone.now() self._finalise_queries() def _silk_installed(self): middlewares = getattr(settings, 'MIDDLEWARE', []) if not middlewares: middlewares = [] middleware_installed = SilkyConfig().SILKY_MIDDLEWARE_CLASS in middlewares return apps.is_installed('silk') and middleware_installed def _should_profile(self): return DataCollector().request is not None def __call__(self, target): if self._silk_installed(): def decorator(view_func): @wraps(view_func) def wrapped_target(*args, **kwargs): with silk_meta_profiler(): try: func_code = target.__code__ except AttributeError: raise NotImplementedError('Profile not implemented to decorate type %s' % target.__class__.__name__) line_num = func_code.co_firstlineno file_path = func_code.co_filename func_name = target.__name__ if not self.name: self.name = func_name self.profile = { 'func_name': func_name, 'name': self.name, 'file_path': file_path, 'line_num': line_num, 'dynamic': self._dynamic, 'start_time': timezone.now(), 'request': DataCollector().request } self._start_queries() try: result = target(*args, **kwargs) except Exception: self.profile['exception_raised'] = True raise finally: with silk_meta_profiler(): self.profile['end_time'] = timezone.now() self._finalise_queries() return result return wrapped_target return decorator(target) else: logger.warning('Cannot execute silk_profile as silk is not installed correctly.') return target def distinct_queries(self): return [x for x in self._queries_after if x not in self._queries_before] @silk_profile() def blah(): time.sleep(1) if __name__ == '__main__': blah() ================================================ FILE: silk/request_filters.py ================================================ """ Django queryset filters used by the requests view """ import logging from datetime import datetime, timedelta from django.db.models import Count, Q, Sum from django.utils import timezone from silk.profiling.dynamic import _get_module from silk.templatetags.silk_filters import _silk_date_time logger = logging.getLogger('silk.request_filters') class FilterValidationError(Exception): pass class BaseFilter(Q): def __init__(self, value=None, *args, **kwargs): self.value = value super().__init__(*args, **kwargs) @property def typ(self): return self.__class__.__name__ @property def serialisable_value(self): return self.value def as_dict(self): return {'typ': self.typ, 'value': self.serialisable_value, 'str': str(self)} @staticmethod def from_dict(d): typ = d['typ'] filter_class = globals()[typ] val = d.get('value', None) return filter_class(val) def contribute_to_query_set(self, query_set): """ make any changes to the query-set before the query is applied, e.g. annotate with extra fields :param query_set: a django queryset :return: a new query set that this filter can then be used with """ return query_set class SecondsFilter(BaseFilter): def __init__(self, n): if n: try: value = int(n) except ValueError as e: raise FilterValidationError(e) now = timezone.now() frm_dt = now - timedelta(seconds=value) super().__init__(value, start_time__gt=frm_dt) else: # Empty query super().__init__() def __str__(self): return '>%d seconds ago' % self.value def _parse(dt, fmt): """attempt to coerce dt into a datetime given fmt, otherwise raise a FilterValidationError""" try: dt = datetime.strptime(dt, fmt) except TypeError: if not isinstance(dt, datetime): raise FilterValidationError('Must be a datetime object') except ValueError as e: raise FilterValidationError(e) return dt class BeforeDateFilter(BaseFilter): fmt = '%Y/%m/%d %H:%M' def __init__(self, dt): value = _parse(dt, self.fmt) super().__init__(value, start_time__lt=value) @property def serialisable_value(self): return self.value.strftime(self.fmt) def __str__(self): return '<%s' % _silk_date_time(self.value) class AfterDateFilter(BaseFilter): fmt = '%Y/%m/%d %H:%M' def __init__(self, dt): value = _parse(dt, self.fmt) super().__init__(value, start_time__gt=value) @property def serialisable_value(self): return self.value.strftime(self.fmt) def __str__(self): return '>%s' % _silk_date_time(self.value) class ViewNameFilter(BaseFilter): """filter on the name of the view, e.g. the name=xyz component of include in urls.py""" def __init__(self, view_name): value = view_name super().__init__(value, view_name=view_name) def __str__(self): return 'View == %s' % self.value class PathFilter(BaseFilter): """filter on path e.g. /path/to/something""" def __init__(self, path): value = path super().__init__(value, path=path) def __str__(self): return 'Path == %s' % self.value class NameFilter(BaseFilter): def __init__(self, name): value = name super().__init__(value, name=name) def __str__(self): return 'name == %s' % self.value class FunctionNameFilter(BaseFilter): def __init__(self, func_name): value = func_name super().__init__(value, func_name=func_name) def __str__(self): return 'func_name == %s' % self.value class NumQueriesFilter(BaseFilter): def __init__(self, n): try: value = int(n) except ValueError as e: raise FilterValidationError(e) super().__init__(value, num_queries__gte=n) def __str__(self): return '#queries >= %s' % self.value def contribute_to_query_set(self, query_set): return query_set.annotate(num_queries=Count('queries')) class TimeSpentOnQueriesFilter(BaseFilter): def __init__(self, n): try: value = int(n) except ValueError as e: raise FilterValidationError(e) super().__init__(value, db_time__gte=n) def __str__(self): return 'DB Time >= %s' % self.value def contribute_to_query_set(self, query_set): return query_set.annotate(db_time=Sum('queries__time_taken')) class OverallTimeFilter(BaseFilter): def __init__(self, n): try: value = int(n) except ValueError as e: raise FilterValidationError(e) super().__init__(value, time_taken__gte=n) def __str__(self): return 'Time >= %s' % self.value class StatusCodeFilter(BaseFilter): def __init__(self, n): try: value = int(n) except ValueError as e: raise FilterValidationError(e) super().__init__(value, response__status_code=n) class MethodFilter(BaseFilter): def __init__(self, value): super().__init__(value, method=value) def filters_from_request(request): raw_filters = {} for key in request.POST: splt = key.split('-') if splt[0].startswith('filter'): ident = splt[1] typ = splt[2] if ident not in raw_filters: raw_filters[ident] = {} raw_filters[ident][typ] = request.POST[key] filters = {} for ident, raw_filter in raw_filters.items(): value = raw_filter.get('value', '') if value.strip(): typ = raw_filter['typ'] module = _get_module('silk.request_filters') filter_class = getattr(module, typ) try: f = filter_class(value) filters[ident] = f except FilterValidationError: logger.warning(f'Validation error when processing filter {typ}({value})') return filters class FiltersManager: def __init__(self, filters_key): self.key = filters_key def save(self, request, filters): if hasattr(request, 'session'): request.session[self.key] = filters request.silk_filters = filters def get(self, request): if hasattr(request, 'session'): return request.session.get(self.key, {}) return request.silk_filters ================================================ FILE: silk/singleton.py ================================================ __author__ = 'mtford' class Singleton(type, metaclass=object): def __init__(cls, name, bases, d): super().__init__(name, bases, d) cls.instance = None def __call__(cls, *args): if cls.instance is None: cls.instance = super().__call__(*args) return cls.instance ================================================ FILE: silk/sql.py ================================================ import logging import traceback from django.core.exceptions import EmptyResultSet from django.utils import timezone from django.utils.encoding import force_str from silk.collector import DataCollector from silk.config import SilkyConfig Logger = logging.getLogger('silk.sql') def _should_wrap(sql_query): if not DataCollector().request: return False for ignore_str in SilkyConfig().SILKY_IGNORE_QUERIES: if ignore_str in sql_query: return False return True def _unpack_explanation(result): for row in result: if not isinstance(row, str): yield ' '.join(str(c) for c in row) else: yield row def _explain_query(connection, q, params): if connection.features.supports_explaining_query_execution: if SilkyConfig().SILKY_ANALYZE_QUERIES: # Work around some DB engines not supporting analyze option try: prefix = connection.ops.explain_query_prefix( analyze=True, **(SilkyConfig().SILKY_EXPLAIN_FLAGS or {}) ) except ValueError as error: error_str = str(error) if error_str.startswith("Unknown options:"): Logger.warning( "Database does not support analyzing queries with provided params. %s." "SILKY_ANALYZE_QUERIES option will be ignored", error_str ) prefix = connection.ops.explain_query_prefix() else: raise error else: prefix = connection.ops.explain_query_prefix() # currently we cannot use explain() method # for queries other than `select` if q.upper().startswith(prefix.upper()): # to avoid "EXPLAIN EXPLAIN", do not add prefix prefixed_query = q else: prefixed_query = f"{prefix} {q}" with connection.cursor() as cur: cur.execute(prefixed_query, params) result = _unpack_explanation(cur.fetchall()) return '\n'.join(result) return None def execute_sql(self, *args, **kwargs): """wrapper around real execute_sql in order to extract information""" try: q, params = self.as_sql() if not q: raise EmptyResultSet except EmptyResultSet: try: result_type = args[0] except IndexError: result_type = kwargs.get('result_type', 'multi') if result_type == 'multi': return iter([]) else: return try: sql_query = q % tuple(force_str(param) for param in params) except UnicodeDecodeError: # Sometimes `force_str` can still raise a UnicodeDecodeError # Reference: https://github.com/jazzband/django-silk/issues?q=encoding # This could log a warning but given this is run in the hot path, logging could be too expensive. return self._execute_sql(*args, **kwargs) if _should_wrap(sql_query): tb = ''.join(reversed(traceback.format_stack())) query_dict = { 'query': sql_query, 'start_time': timezone.now(), 'traceback': tb } try: return self._execute_sql(*args, **kwargs) finally: query_dict['end_time'] = timezone.now() request = DataCollector().request if request: query_dict['request'] = request if getattr(self.query.model, '__module__', '') != 'silk.models': query_dict['analysis'] = _explain_query(self.connection, q, params) DataCollector().register_query(query_dict) else: DataCollector().register_silk_query(query_dict) return self._execute_sql(*args, **kwargs) ================================================ FILE: silk/static/silk/css/components/cell.css ================================================ .cell { display: inline-block; background-color: transparent; padding: 10px; margin-left: 10px; margin-top: 10px; border-radius: 4px; transition: background-color 0.15s ease, color 0.15s ease; } .cell div { margin: 2px; } .cell .timestamp-div { margin-bottom: 15px; font-size: 13px; } .cell .meta { font-size: 12px; color: #be5b43; } .cell .meta .unit { font-size: 9px; font-weight: lighter !important; } .cell .method-div { font-weight: bold; font-size: 20px; } .cell .path-div { font-size: 18px; margin-bottom: 15px; } ================================================ FILE: silk/static/silk/css/components/colors.css ================================================ .very-good-font-color { color: #bac54b; } .good-font-color { color: #c3a948; } .ok-font-color { color: #c08245; } .bad-font-color { color: #be5b43; } .very-bad-font-color { color: #b9424f; } ================================================ FILE: silk/static/silk/css/components/fonts.css ================================================ /** * Fira Sans */ @font-face { font-family: FiraSans; src: url(../../fonts/fira/FiraSans-Regular.woff); font-weight: normal; } @font-face { font-family: FiraSans; src: url(../../fonts/fira/FiraSans-Medium.woff); font-weight: bold; } @font-face { font-family: FiraSans; src: url(../../fonts/fira/FiraSans-Bold.woff); font-weight: bolder; } @font-face { font-family: FiraSans; src: url(../../fonts/fira/FiraSans-Light.woff); font-weight: lighter; } @font-face { font-family: FiraSans; src: url(../../fonts/fira/FiraSans-RegularItalic.woff); font-weight: normal; font-style: italic; } @font-face { font-family: FiraSans; src: url(../../fonts/fira/FiraSans-MediumItalic.woff); font-weight: bold; font-style: italic; } @font-face { font-family: FiraSans; src: url(../../fonts/fira/FiraSans-BoldItalic.woff); font-weight: bolder; font-style: italic; } @font-face { font-family: FiraSans; src: url(../../fonts/fira/FiraSans-LightItalic.woff); font-weight: lighter; font-style: italic; } /** * Fantasque */ @font-face { font-family: Fantasque; src: url(../../fonts/fantasque/FantasqueSansMono-Regular.woff); font-weight: normal; } @font-face { font-family: Fantasque; src: url(../../fonts/fantasque/FantasqueSansMono-Bold.woff); font-weight: bold; } @font-face { font-family: Fantasque; src: url(../../fonts/fantasque/FantasqueSansMono-RegItalic.woff); font-weight: normal; font-style: italic; } @font-face { font-family: Fantasque; src: url(../../fonts/fantasque/FantasqueSansMono-BoldItalic.woff); font-weight: bold; font-style: italic; } ================================================ FILE: silk/static/silk/css/components/heading.css ================================================ .heading { width: 100%; background-color: transparent; height: 30px; display: table; font-weight: bold; margin-top: 20px; } .heading .inner-heading { display: table-cell; text-align: left; padding: 0; vertical-align: middle; } ================================================ FILE: silk/static/silk/css/components/numeric.css ================================================ .numeric { font-weight: normal; } .unit { font-weight: normal; } .numeric .unit { font-size: 12px; } .numeric { font-size: 20px; } ================================================ FILE: silk/static/silk/css/components/row.css ================================================ .row-wrapper { display: table; margin: 2rem; width: 100%; width: -moz-available; width: -webkit-fill-available; width: fill-available; } .row-wrapper .row { display: table-row; transition: background-color 0.15s ease, color 0.15s ease; } .row-wrapper .row div { padding: 1rem; } .row-wrapper .row .col { font-size: 20px; display: table-cell; } .row-wrapper .row .timestamp-div { border-top-left-radius: 4px; border-bottom-left-radius: 4px; margin-bottom: 15px; font-size: 13px; } .row-wrapper .row .meta { font-size: 12px; color: #be5b43; } .row-wrapper .row .meta .unit { font-size: 9px; font-weight: lighter !important; } .row-wrapper .row .method-div { font-weight: bold; font-size: 20px; } .row-wrapper .row .path-div { font-size: 18px; margin-bottom: 15px; } .row-wrapper .row .num-queries-div { border-top-right-radius: 4px; border-bottom-right-radius: 4px; } .row-wrapper .row .spacing .numeric { padding: 0 0.3rem; } .row-wrapper .row .spacing .meta { padding: 0 0.3rem; } .row-wrapper .row:hover { background-color: rgb(51, 51, 68); color: white; cursor: pointer; } ================================================ FILE: silk/static/silk/css/components/summary.css ================================================ #error-div { margin: 10px; } #query-div { margin: auto; width: 960px; text-align: center; } #code { text-align: left; } .name-div { margin-top: 20px; margin-bottom: 15px; font-weight: bold; } .description { text-align: left; } ================================================ FILE: silk/static/silk/css/pages/base.css ================================================ body { font-family: FiraSans, "Helvetica Neue", Arial, sans-serif; background-color: #f3f3f3; margin: 0; font-weight: lighter; } pre { font-family: Fantasque; background-color: white !important; padding: 0.5em !important; margin: 0 !important; font-size: 14px; text-align: left; } code { font-family: Fantasque; background-color: white !important; padding: 0 !important; margin: 0 !important; font-size: 14px; } html { margin: 0; } #header { height: 50px; background-color: rgb(51, 51, 68); width: 100%; position: relative; padding: 0; } #header div { display: inline-block; } .menu { height: 50px; padding: 0; margin: 0; } .menu-item { height: 50px; padding-left: 10px; padding-right: 10px; margin: 0; margin-right: -4px; color: white; } .menu-item a { color: white !important; } #filter .menu-item { margin-right: 0px; } .selectable-menu-item { transition: background-color 0.15s ease, color 0.15s ease; } .selectable-menu-item:hover { background-color: #f3f3f3; cursor: pointer; color: black !important; } .selectable-menu-item:hover a { color: black !important; } .menu-item-selected { background-color: #f3f3f3; color: black !important; } .menu-item-selected a { color: black !important; } .menu-item-outer { display: table !important; height: 100%; width: 100%; } .menu-item-inner { display: table-cell !important; vertical-align: middle; width: 100%; } a:visited { color: black; } a { color: black; text-decoration: none; } #filter { height: 50px; position: absolute; right: 0; } .description { font-style: italic; font-size: 14px; margin-bottom: 5px; } ================================================ FILE: silk/static/silk/css/pages/clear_db.css ================================================ .wrapper { width: 100%; margin-bottom: 20px; } .inner { margin: auto; width: 960px; } .cleardb-form .cleardb-form-wrapper { margin-bottom: 20px; } .cleardb-form label { display: block; margin-bottom: 8px; } .cleardb-form .btn { background: #333344; color: #fff; padding: 10px 20px; border-radius: 2px; cursor: pointer; box-shadow: none; font-size: 16px; line-height: 20px; border: 0; min-width: 150px; text-align: center; } .cleardb-form label :last-child { margin-bottom: 0; } .msg { margin-top: 20px; color: #bac54b; } ================================================ FILE: silk/static/silk/css/pages/cprofile.css ================================================ #query-info-div { margin-top: 15px; } #query-info-div .timestamp-div { font-size: 13px; } #pyprofile-div { display: block; margin: auto; width: 960px; } .pyprofile { text-align: left; } a { color: #45ADA8; } a:visited { color: #45ADA8; } a:hover { color: #547980; } a:active { color: #594F4F; } #graph-div { padding: 25px; background-color: white; display: block; margin-left: auto; margin-right: auto; margin-top: 25px; width: 960px; text-align: center; } #percent { width: 20px; } svg { display: block; } ================================================ FILE: silk/static/silk/css/pages/detail_base.css ================================================ #traceback { overflow: visible; } #time-div { text-align: center; margin-bottom: 30px; } #query-div { text-align: center; margin-bottom: 20px; } #query { text-align: left; margin: 0 auto; display: inline-block; } .line { width: 100%; display: inline-block; } .the-line { background-color: #c3c3c3; } pre { margin: 0; } ================================================ FILE: silk/static/silk/css/pages/profile_detail.css ================================================ #query-info-div { margin-top: 15px; } #query-info-div .timestamp-div { font-size: 13px; } #pyprofile-div { display: block; margin: auto; width: 960px; } .pyprofile { text-align: left; } a { color: #45ADA8; } a:visited { color: #45ADA8; } a:hover { color: #547980; } a:active { color: #594F4F; } #graph-div { padding: 25px; background-color: white; display: block; margin-left: auto; margin-right: auto; margin-top: 25px; width: 960px; text-align: center; } #percent { width: 20px; } svg { display: block; } ================================================ FILE: silk/static/silk/css/pages/profiling.css ================================================ .name-div { font-weight: bold; } .container { padding: 0 1em; } h2 { margin-bottom: 10px; } .pyprofile { overflow: scroll; max-height: 650px; } ================================================ FILE: silk/static/silk/css/pages/raw.css ================================================ pre { width: 100%; height: 100%; margin: 0; padding: 0; background-color: white !important; white-space: pre-wrap; /* css-3 */ white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ /*noinspection CssInvalidElement*/ white-space: -pre-wrap; /* Opera 4-6 */ white-space: -o-pre-wrap; /* Opera 7 */ word-wrap: break-word; /* Internet Explorer 5.5+ */ } code { font-family: Fantasque; background-color: white !important; width: 100% !important; height: auto; padding: 0 !important; } body { margin: 0; padding: 0; } html { margin: 0; padding: 0; } ================================================ FILE: silk/static/silk/css/pages/request.css ================================================ pre { white-space: pre-wrap; /* css-3 */ white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ /*noinspection CssInvalidElement*/ white-space: -pre-wrap; /* Opera 4-6 */ white-space: -o-pre-wrap; /* Opera 7 */ word-wrap: break-word; /* Internet Explorer 5.5+ */ } .cell { background-color: transparent; margin-top: 15px; } div.wrapper { width: 100%; } div.wrapper div#request-summary { margin: auto; text-align: center; width: 100%; } div.wrapper div#request-info { width: 960px; margin: auto auto 20px; } a { color: #45ADA8; } a:visited { color: #45ADA8; } a:hover { color: #547980; } a:active { color: #594F4F; } .headers { font-size: 12px; font-family: Fantasque; background-color: white; width: 100%; } .headers tr:hover { background-color: #f4f4f4; } .headers td { padding-bottom: 5px; padding-left: 5px; } .headers .key { font-weight: bold; } ================================================ FILE: silk/static/silk/css/pages/requests.css ================================================ .container { padding: 0 1em; } .resizing-input input { background-color: white; padding-top: 2px; color: black; box-shadow: inset 0 0 3px black; } .resizing-input input::placeholder { color: #383838; opacity: 1; } .filter-section { line-height: 2.3; } ================================================ FILE: silk/static/silk/css/pages/root_base.css ================================================ .cell:hover { background-color: rgb(51, 51, 68); color: white; cursor: pointer; } .cell { text-align: center; } /* General styles for all menus */ .cbp-spmenu { background: rgb(51, 51, 68); position: fixed; } h3 { color: white; font-size: 1.9em; padding: 10px; margin: 0; font-weight: 300; background: rgb(51, 51, 68); } /*.cbp-spmenu div {*/ /*display: block;*/ /*color: #fff;*/ /*font-size: 1.1em;*/ /*font-weight: 300;*/ /*}*/ /* Orientation-dependent styles for the content of the menu */ .cbp-spmenu-vertical { width: 300px; height: 100%; top: 0; z-index: 1000; } /* Vertical menu that slides from the left or right */ .cbp-spmenu-right { right: -300px; } .cbp-spmenu-right.cbp-spmenu-open { right: 0px; } /* Push classes applied to the body */ .cbp-spmenu-push { overflow-x: hidden; position: relative; left: 0; } .cbp-spmenu-push-toleft { left: -300px; } /* Transitions */ .cbp-spmenu, .cbp-spmenu-push { -webkit-transition: all 0.3s ease; -moz-transition: all 0.3s ease; transition: all 0.3s ease; } /* Example media queries */ @media screen and (max-width: 55.1875em) { .cbp-spmenu-horizontal { font-size: 75%; height: 110px; } } .active-filter { font-size: 12px; color: white; } .active-filter td { padding: 0; margin: 0; } #cbp-spmenu-s2 button { color: white; background: none; border: none; } #cbp-spmenu-s2 button:hover { color: black; background: white; border: none; cursor: pointer; } #cbp-spmenu-s2 button:focus { outline: none; } #slider-label { margin-top: 5px; } #outer-slider { text-align: center; width: 80%; } #seconds { width: 200px; height: 50px; } #slider { width: 80%; margin: auto; margin-top: 10px; } input[type=number], input[type=text] { border: none; padding: 10px; width: 222px; } .add-button { font-size: 20px; font-weight: bold; } .add-button.disabled { color: darkgray !important; } .add-button.disabled:hover { background: transparent !important; cursor: default !important; } .button-div { display: inline-block; text-align: center; height: 50px; } .apply-div { display: inline-block; background-color: white; color: black; margin: 10px; padding: 10px; border-radius: 5px; } .apply-div:hover { cursor: pointer; } select { border-radius: 0; max-width: 100%; } @media screen and (-webkit-min-device-pixel-ratio: 0) { /*safari and chrome*/ select { line-height: 30px; background: #f4f4f4; } } select::-moz-focus-inner { /*Remove button padding in FF*/ border: 0; padding: 0; } #cbp-spmenu-s2 p { margin-bottom: 10px; margin-left: 10px; margin-right: 10px; margin-top: 0; font-size: 12px; color: white; font-style: oblique; line-height: 16px; } #filter-item { width: 24px; height: 20px; float: right; display: inline; background-image: url("../../filter.png"); background-size: contain; background-repeat: no-repeat; text-align: right; } #filter-item #num-filters { position: relative; top: 7px; left: 1px; } .resizing-input { display: inline-block; } .resizing-input input, .resizing-input span { font-size: 12px; font-family: FiraSans; white-space: pre; font-weight: 300; background-color: transparent; color: white; padding: 0; padding-left: 4px; padding-right: 4px; margin: 0; letter-spacing: 1px; } .resizing-input input:focus { outline: none; } h4 { padding: 0; margin: 3px; margin-left: 10px; color: white; font-weight: lighter; font-size: 20px; } .filter-section { color: white; font-size: 12px; line-height: 2.3; padding: 10px; } .filter-section select { margin-bottom: 10px; } .filter-section input { padding: 4px; } ================================================ FILE: silk/static/silk/css/pages/sql.css ================================================ .right-aligned { text-align: right; } .center-aligned { text-align: center; } .left-aligned { text-align: left; } #table-pagination { text-align: center; margin: 20px; } table { border-spacing: 0; margin: auto; max-width: 920px; } tr { height: 20px; } tr.data-row:hover { background-color: rgb(51, 51, 68); color: white; cursor: pointer; } td { padding: 5px; padding-left: 20px; padding-right: 20px; } th { height: 40px; padding-left: 20px; padding-right: 20px; } #table-div { width: 100%; margin-top: 40px; } #table-pagination div { padding: 5px; } ================================================ FILE: silk/static/silk/css/pages/sql_detail.css ================================================ #traceback { width: 960px; margin: auto; } #traceback pre { margin-top: 15px !important; margin-bottom: 15px !important; } #traceback .not-third-party { font-weight: bold; } a:hover { color: #9dd0ff; } a:active { color: #594F4F; } code { background-color: transparent !important; } #query-div pre { background-color: transparent !important; } #query-div { padding-top: 15px; } #query-info-div div { padding-top: 5px; } #query-plan-div { text-align: left; width: 960px; margin: auto; } #plan-div code { margin: auto !important; display: inline-block; } #query-plan-head { padding-top: 5px; padding-bottom: 15px; text-align: center; margin: auto; } .file-path { font-size: 13px; } ================================================ FILE: silk/static/silk/css/pages/summary.css ================================================ .wrapper { width: 100%; margin-bottom: 20px; } .inner { margin: auto; width: 960px; } .summary-cell { display: inline-block; text-align: center; padding: 10px; margin-left: 10px; margin-top: 10px; } .summary-cell .desc { margin-top: 8px; font-size: 12px; } .no-data { font-size: 12px; font-style: oblique; margin-left: 10px; } h2 { margin-bottom: 0; } #filters { margin-top: 10px; font-size: 12px; } #filters input, #filters span { color: black !important; font-weight: bold; } #filter-image { width: 20px; } #filter-cell { padding-left: 5px; } ================================================ FILE: silk/static/silk/js/components/cell.js ================================================ function configureSpanFontColors(selector, okValue, badValue) { selector.each(function () { var val = parseFloat($(this).text()); if (val < okValue) { $(this).addClass('very-good-font-color'); } else if (val < badValue) { $(this).addClass('ok-font-color'); } else { $(this).addClass('very-bad-font-color'); } }); } function configureFontColors() { configureSpanFontColors($('.time-taken-div .numeric'), 200, 500); configureSpanFontColors($('.time-taken-queries-div .numeric'), 50, 200); configureSpanFontColors($('.num-queries-div .numeric'), 10, 50); } $(document).ready(function () { configureFontColors(); }); ================================================ FILE: silk/static/silk/js/components/filters.js ================================================ function configureResizingInputs() { var $inputs = $('.resizing-input'); function resizeForText(text) { var $this = $(this); if (!text.trim()) { text = $this.attr('placeholder').trim(); } var $span = $this.parent().find('span'); $span.text(text); var $inputSize = $span.width(); $this.css("width", $inputSize); } $inputs.find('input').keypress(function (e) { if (e.which && e.charCode) { var c = String.fromCharCode(e.keyCode | e.charCode); var $this = $(this); resizeForText.call($this, $this.val() + c); } }); $inputs.find('input').keyup(function (e) { // Backspace event only fires for keyup if (e.keyCode === 8 || e.keyCode === 46) { resizeForText.call($(this), $(this).val()); } }); $inputs.find('input').each(function () { var $this = $(this); resizeForText.call($this, $this.val()) }); $('.resizing-input .datetimepicker').datetimepicker({ step: 10, onChangeDateTime: function (dp, $input) { resizeForText.call($input, $input.val()) } }); } /** * Entry point for filter initialisation. */ function initFilters() { configureResizingInputs(); } ================================================ FILE: silk/static/silk/js/pages/base.js ================================================ $(document).ready(function () { configureSpanFontColors($('#num-joins-div').find('.numeric'), 3, 5); configureSpanFontColors($('#time-taken-div').find('.numeric'), 200, 500); configureSpanFontColors($('#num-queries-div').find('.numeric'), 10, 500); }); ================================================ FILE: silk/static/silk/js/pages/clear_db.js ================================================ $(document).ready(function () { initFilters(); var $inputs = $('.resizing-input'); $inputs.focusout(function () { $('#filter-form').submit(); }); }); ================================================ FILE: silk/static/silk/js/pages/detail_base.js ================================================ hljs.initHighlightingOnLoad(); ================================================ FILE: silk/static/silk/js/pages/profile_detail.js ================================================ function createViz() { var profileDotURL = JSON.parse(document.getElementById("profileDotURL").textContent); $.get( profileDotURL, { cutoff: $('#percent').val() }, function (response) { var svg = '#graph-div'; $(svg).html(Viz(response.dot)); $(svg + ' svg').attr('width', 960).attr('height', 600); svgPanZoom(svg + ' svg', { controlIconsEnabled: true }); } ); } createViz(); ================================================ FILE: silk/static/silk/js/pages/profiling.js ================================================ $(document).ready(function () { initFilters(); initFilterButton(); }); ================================================ FILE: silk/static/silk/js/pages/raw.js ================================================ hljs.initHighlightingOnLoad(); ================================================ FILE: silk/static/silk/js/pages/request.js ================================================ hljs.initHighlightingOnLoad(); ================================================ FILE: silk/static/silk/js/pages/requests.js ================================================ $(document).ready(function () { initFilters(); initFilterButton(); }); ================================================ FILE: silk/static/silk/js/pages/root_base.js ================================================ function initFilterButton() { $('#filter-button').click(function () { $(this).toggleClass('active'); $('body').toggleClass('cbp-spmenu-push-toleft'); $('#cbp-spmenu-s2').toggleClass('cbp-spmenu-open'); initFilters(); }); } function submitFilters() { $('#filter-form2').submit(); } function submitEmptyFilters() { $('#cbp-spmenu-s2 :input:not([type=hidden])').val(''); submitFilters(); } ================================================ FILE: silk/static/silk/js/pages/sql.js ================================================ $(document).ready(function () { document.querySelectorAll(".data-row").forEach((rowElement) => { let sqlDetailUrl = rowElement.dataset.sqlDetailUrl; rowElement.addEventListener("mouseup", (e) => { switch (e.button) { case 0: window.location = sqlDetailUrl; break; case 1: window.open(sqlDetailUrl); break; default: break; } }); }); }); ================================================ FILE: silk/static/silk/js/pages/sql_detail.js ================================================ $(document).ready(function () { configureSpanFontColors($('#num-joins-div').find('.numeric'), 3, 5); configureSpanFontColors($('#time-taken-div').find('.numeric'), 200, 500); }); ================================================ FILE: silk/static/silk/js/pages/summary.js ================================================ $(document).ready(function () { initFilters(); var $inputs = $('.resizing-input'); $inputs.focusout(function () { $('#filter-form').submit(); }); }); ================================================ FILE: silk/static/silk/lib/highlight/foundation.css ================================================ /* Description: Foundation 4 docs style for highlight.js Author: Dan Allen Website: http://foundation.zurb.com/docs/ Version: 1.0 Date: 2013-04-02 */ .hljs { display: block; padding: 0.5em; background: #eee; } .hljs-header, .hljs-decorator, .hljs-annotation { color: #000077; } .hljs-horizontal_rule, .hljs-link_url, .hljs-emphasis, .hljs-attribute { color: #070; } .hljs-emphasis { font-style: italic; } .hljs-link_label, .hljs-strong, .hljs-value, .hljs-string, .scss .hljs-value .hljs-string { color: #d14; } .hljs-strong { font-weight: bold; } .hljs-blockquote, .hljs-comment { color: #998; font-style: italic; } .asciidoc .hljs-title, .hljs-function .hljs-title { color: #900; } .hljs-class { color: #458; } .hljs-id, .hljs-pseudo, .hljs-constant, .hljs-hexcolor { color: teal; } .hljs-variable { color: #336699; } .hljs-bullet, .hljs-javadoc { color: #997700; } .hljs-pi, .hljs-doctype { color: #3344bb; } .hljs-code, .hljs-number { color: #099; } .hljs-important { color: #f00; } .smartquote, .hljs-label { color: #970; } .hljs-preprocessor, .hljs-pragma { color: #579; } .hljs-reserved, .hljs-keyword, .scss .hljs-value { color: #000; } .hljs-regexp { background-color: #fff0ff; color: #880088; } .hljs-symbol { color: #990073; } .hljs-symbol .hljs-string { color: #a60; } .hljs-tag { color: #007700; } .hljs-at_rule, .hljs-at_rule .hljs-keyword { color: #088; } .hljs-at_rule .hljs-preprocessor { color: #808; } .scss .hljs-tag, .scss .hljs-attribute { color: #339; } ================================================ FILE: silk/static/silk/lib/highlight/highlight.pack.js ================================================ var hljs=new function(){function k(v){return v.replace(/&/gm,"&").replace(//gm,">")}function t(v){return v.nodeName.toLowerCase()}function i(w,x){var v=w&&w.exec(x);return v&&v.index==0}function d(v){return Array.prototype.map.call(v.childNodes,function(w){if(w.nodeType==3){return b.useBR?w.nodeValue.replace(/\n/g,""):w.nodeValue}if(t(w)=="br"){return"\n"}return d(w)}).join("")}function r(w){var v=(w.className+" "+(w.parentNode?w.parentNode.className:"")).split(/\s+/);v=v.map(function(x){return x.replace(/^language-/,"")});return v.filter(function(x){return j(x)||x=="no-highlight"})[0]}function o(x,y){var v={};for(var w in x){v[w]=x[w]}if(y){for(var w in y){v[w]=y[w]}}return v}function u(x){var v=[];(function w(y,z){for(var A=y.firstChild;A;A=A.nextSibling){if(A.nodeType==3){z+=A.nodeValue.length}else{if(t(A)=="br"){z+=1}else{if(A.nodeType==1){v.push({event:"start",offset:z,node:A});z=w(A,z);v.push({event:"stop",offset:z,node:A})}}}}return z})(x,0);return v}function q(w,y,C){var x=0;var F="";var z=[];function B(){if(!w.length||!y.length){return w.length?w:y}if(w[0].offset!=y[0].offset){return(w[0].offset"}function E(G){F+=""}function v(G){(G.event=="start"?A:E)(G.node)}while(w.length||y.length){var D=B();F+=k(C.substr(x,D[0].offset-x));x=D[0].offset;if(D==w){z.reverse().forEach(E);do{v(D.splice(0,1)[0]);D=B()}while(D==w&&D.length&&D[0].offset==x);z.reverse().forEach(A)}else{if(D[0].event=="start"){z.push(D[0].node)}else{z.pop()}v(D.splice(0,1)[0])}}return F+k(C.substr(x))}function m(y){function v(z){return(z&&z.source)||z}function w(A,z){return RegExp(v(A),"m"+(y.cI?"i":"")+(z?"g":""))}function x(D,C){if(D.compiled){return}D.compiled=true;D.k=D.k||D.bK;if(D.k){var z={};function E(G,F){if(y.cI){F=F.toLowerCase()}F.split(" ").forEach(function(H){var I=H.split("|");z[I[0]]=[G,I[1]?Number(I[1]):1]})}if(typeof D.k=="string"){E("keyword",D.k)}else{Object.keys(D.k).forEach(function(F){E(F,D.k[F])})}D.k=z}D.lR=w(D.l||/\b[A-Za-z0-9_]+\b/,true);if(C){if(D.bK){D.b=D.bK.split(" ").join("|")}if(!D.b){D.b=/\B|\b/}D.bR=w(D.b);if(!D.e&&!D.eW){D.e=/\B|\b/}if(D.e){D.eR=w(D.e)}D.tE=v(D.e)||"";if(D.eW&&C.tE){D.tE+=(D.e?"|":"")+C.tE}}if(D.i){D.iR=w(D.i)}if(D.r===undefined){D.r=1}if(!D.c){D.c=[]}var B=[];D.c.forEach(function(F){if(F.v){F.v.forEach(function(G){B.push(o(F,G))})}else{B.push(F=="self"?D:F)}});D.c=B;D.c.forEach(function(F){x(F,D)});if(D.starts){x(D.starts,C)}var A=D.c.map(function(F){return F.bK?"\\.?\\b("+F.b+")\\b\\.?":F.b}).concat([D.tE]).concat([D.i]).map(v).filter(Boolean);D.t=A.length?w(A.join("|"),true):{exec:function(F){return null}};D.continuation={}}x(y)}function c(S,L,J,R){function v(U,V){for(var T=0;T";U+=Z+'">';return U+X+Y}function N(){var U=k(C);if(!I.k){return U}var T="";var X=0;I.lR.lastIndex=0;var V=I.lR.exec(U);while(V){T+=U.substr(X,V.index-X);var W=E(I,V);if(W){H+=W[1];T+=w(W[0],V[0])}else{T+=V[0]}X=I.lR.lastIndex;V=I.lR.exec(U)}return T+U.substr(X)}function F(){if(I.sL&&!f[I.sL]){return k(C)}var T=I.sL?c(I.sL,C,true,I.continuation.top):g(C);if(I.r>0){H+=T.r}if(I.subLanguageMode=="continuous"){I.continuation.top=T.top}return w(T.language,T.value,false,true)}function Q(){return I.sL!==undefined?F():N()}function P(V,U){var T=V.cN?w(V.cN,"",true):"";if(V.rB){D+=T;C=""}else{if(V.eB){D+=k(U)+T;C=""}else{D+=T;C=U}}I=Object.create(V,{parent:{value:I}})}function G(T,X){C+=T;if(X===undefined){D+=Q();return 0}var V=v(X,I);if(V){D+=Q();P(V,X);return V.rB?0:X.length}var W=z(I,X);if(W){var U=I;if(!(U.rE||U.eE)){C+=X}D+=Q();do{if(I.cN){D+=""}H+=I.r;I=I.parent}while(I!=W.parent);if(U.eE){D+=k(X)}C="";if(W.starts){P(W.starts,"")}return U.rE?0:X.length}if(A(X,I)){throw new Error('Illegal lexeme "'+X+'" for mode "'+(I.cN||"")+'"')}C+=X;return X.length||1}var M=j(S);if(!M){throw new Error('Unknown language: "'+S+'"')}m(M);var I=R||M;var D="";for(var K=I;K!=M;K=K.parent){if(K.cN){D=w(K.cN,D,true)}}var C="";var H=0;try{var B,y,x=0;while(true){I.t.lastIndex=x;B=I.t.exec(L);if(!B){break}y=G(L.substr(x,B.index-x),B[0]);x=B.index+y}G(L.substr(x));for(var K=I;K.parent;K=K.parent){if(K.cN){D+=""}}return{r:H,value:D,language:S,top:I}}catch(O){if(O.message.indexOf("Illegal")!=-1){return{r:0,value:k(L)}}else{throw O}}}function g(y,x){x=x||b.languages||Object.keys(f);var v={r:0,value:k(y)};var w=v;x.forEach(function(z){if(!j(z)){return}var A=c(z,y,false);A.language=z;if(A.r>w.r){w=A}if(A.r>v.r){w=v;v=A}});if(w.language){v.second_best=w}return v}function h(v){if(b.tabReplace){v=v.replace(/^((<[^>]+>|\t)+)/gm,function(w,z,y,x){return z.replace(/\t/g,b.tabReplace)})}if(b.useBR){v=v.replace(/\n/g,"
")}return v}function p(z){var y=d(z);var A=r(z);if(A=="no-highlight"){return}var v=A?c(A,y,true):g(y);var w=u(z);if(w.length){var x=document.createElementNS("http://www.w3.org/1999/xhtml","pre");x.innerHTML=v.value;v.value=q(w,u(x),y)}v.value=h(v.value);z.innerHTML=v.value;z.className+=" hljs "+(!A&&v.language||"");z.result={language:v.language,re:v.r};if(v.second_best){z.second_best={language:v.second_best.language,re:v.second_best.r}}}var b={classPrefix:"hljs-",tabReplace:null,useBR:false,languages:undefined};function s(v){b=o(b,v)}function l(){if(l.called){return}l.called=true;var v=document.querySelectorAll("pre code");Array.prototype.forEach.call(v,p)}function a(){addEventListener("DOMContentLoaded",l,false);addEventListener("load",l,false)}var f={};var n={};function e(v,x){var w=f[v]=x(this);if(w.aliases){w.aliases.forEach(function(y){n[y]=v})}}function j(v){return f[v]||f[n[v]]}this.highlight=c;this.highlightAuto=g;this.fixMarkup=h;this.highlightBlock=p;this.configure=s;this.initHighlighting=l;this.initHighlightingOnLoad=a;this.registerLanguage=e;this.getLanguage=j;this.inherit=o;this.IR="[a-zA-Z][a-zA-Z0-9_]*";this.UIR="[a-zA-Z_][a-zA-Z0-9_]*";this.NR="\\b\\d+(\\.\\d+)?";this.CNR="(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)";this.BNR="\\b(0b[01]+)";this.RSR="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~";this.BE={b:"\\\\[\\s\\S]",r:0};this.ASM={cN:"string",b:"'",e:"'",i:"\\n",c:[this.BE]};this.QSM={cN:"string",b:'"',e:'"',i:"\\n",c:[this.BE]};this.CLCM={cN:"comment",b:"//",e:"$"};this.CBLCLM={cN:"comment",b:"/\\*",e:"\\*/"};this.HCM={cN:"comment",b:"#",e:"$"};this.NM={cN:"number",b:this.NR,r:0};this.CNM={cN:"number",b:this.CNR,r:0};this.BNM={cN:"number",b:this.BNR,r:0};this.REGEXP_MODE={cN:"regexp",b:/\//,e:/\/[gim]*/,i:/\n/,c:[this.BE,{b:/\[/,e:/\]/,r:0,c:[this.BE]}]};this.TM={cN:"title",b:this.IR,r:0};this.UTM={cN:"title",b:this.UIR,r:0}}();hljs.registerLanguage("python",function(a){var f={cN:"prompt",b:/^(>>>|\.\.\.) /};var b={cN:"string",c:[a.BE],v:[{b:/(u|b)?r?'''/,e:/'''/,c:[f],r:10},{b:/(u|b)?r?"""/,e:/"""/,c:[f],r:10},{b:/(u|r|ur)'/,e:/'/,r:10},{b:/(u|r|ur)"/,e:/"/,r:10},{b:/(b|br)'/,e:/'/,},{b:/(b|br)"/,e:/"/,},a.ASM,a.QSM]};var d={cN:"number",r:0,v:[{b:a.BNR+"[lLjJ]?"},{b:"\\b(0o[0-7]+)[lLjJ]?"},{b:a.CNR+"[lLjJ]?"}]};var e={cN:"params",b:/\(/,e:/\)/,c:["self",f,d,b]};var c={e:/:/,i:/[${=;\n]/,c:[a.UTM,e]};return{k:{keyword:"and elif is global as in if from raise for except finally print import pass return exec else break not with class assert yield try while continue del or def lambda nonlocal|10 None True False",built_in:"Ellipsis NotImplemented"},i:/(<\/|->|\?)/,c:[f,d,b,a.HCM,a.inherit(c,{cN:"function",bK:"def",r:10}),a.inherit(c,{cN:"class",bK:"class"}),{cN:"decorator",b:/@/,e:/$/},{b:/\b(print|exec)\(/}]}});hljs.registerLanguage("sql",function(a){return{cI:true,i:/[<>]/,c:[{cN:"operator",b:"\\b(begin|end|start|commit|rollback|savepoint|lock|alter|create|drop|rename|call|delete|do|handler|insert|load|replace|select|truncate|update|set|show|pragma|grant|merge)\\b(?!:)",e:";",eW:true,k:{keyword:"all partial global month current_timestamp using go revoke smallint indicator end-exec disconnect zone with character assertion to add current_user usage input local alter match collate real then rollback get read timestamp session_user not integer bit unique day minute desc insert execute like ilike|2 level decimal drop continue isolation found where constraints domain right national some module transaction relative second connect escape close system_user for deferred section cast current sqlstate allocate intersect deallocate numeric public preserve full goto initially asc no key output collation group by union session both last language constraint column of space foreign deferrable prior connection unknown action commit view or first into float year primary cascaded except restrict set references names table outer open select size are rows from prepare distinct leading create only next inner authorization schema corresponding option declare precision immediate else timezone_minute external varying translation true case exception join hour default double scroll value cursor descriptor values dec fetch procedure delete and false int is describe char as at in varchar null trailing any absolute current_time end grant privileges when cross check write current_date pad begin temporary exec time update catalog user sql date on identity timezone_hour natural whenever interval work order cascade diagnostics nchar having left call do handler load replace truncate start lock show pragma exists number trigger if before after each row merge matched database",aggregate:"count sum min max avg"},c:[{cN:"string",b:"'",e:"'",c:[a.BE,{b:"''"}]},{cN:"string",b:'"',e:'"',c:[a.BE,{b:'""'}]},{cN:"string",b:"`",e:"`",c:[a.BE]},a.CNM]},a.CBLCLM,{cN:"comment",b:"--",e:"$"}]}});hljs.registerLanguage("json",function(a){var e={literal:"true false null"};var d=[a.QSM,a.CNM];var c={cN:"value",e:",",eW:true,eE:true,c:d,k:e};var b={b:"{",e:"}",c:[{cN:"attribute",b:'\\s*"',e:'"\\s*:\\s*',eB:true,eE:true,c:[a.BE],i:"\\n",starts:c}],i:"\\S"};var f={b:"\\[",e:"\\]",c:[a.inherit(c,{cN:null})],i:"\\S"};d.splice(d.length,0,b,f);return{c:d,k:e,i:"\\S"}}); ================================================ FILE: silk/static/silk/lib/jquery.datetimepicker.css ================================================ .xdsoft_datetimepicker { box-shadow: 0 5px 15px -5px rgba(0, 0, 0, 0.506); background: #fff; border-bottom: 1px solid #bbb; border-left: 1px solid #ccc; border-right: 1px solid #ccc; border-top: 1px solid #ccc; color: #333; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; padding: 8px; padding-left: 0; padding-top: 2px; position: absolute; z-index: 9999; -moz-box-sizing: border-box; box-sizing: border-box; display: none; } .xdsoft_datetimepicker iframe { position: absolute; left: 0; top: 0; width: 75px; height: 210px; background: transparent; border: none; } /*For IE8 or lower*/ .xdsoft_datetimepicker button { border: none !important; } .xdsoft_noselect { -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; -o-user-select: none; user-select: none; } .xdsoft_noselect::selection { background: transparent } .xdsoft_noselect::-moz-selection { background: transparent } .xdsoft_datetimepicker.xdsoft_inline { display: inline-block; position: static; box-shadow: none; } .xdsoft_datetimepicker * { -moz-box-sizing: border-box; box-sizing: border-box; padding: 0; margin: 0; } .xdsoft_datetimepicker .xdsoft_datepicker, .xdsoft_datetimepicker .xdsoft_timepicker { display: none; } .xdsoft_datetimepicker .xdsoft_datepicker.active, .xdsoft_datetimepicker .xdsoft_timepicker.active { display: block; } .xdsoft_datetimepicker .xdsoft_datepicker { width: 224px; float: left; margin-left: 8px; } .xdsoft_datetimepicker.xdsoft_showweeks .xdsoft_datepicker { width: 256px; } .xdsoft_datetimepicker .xdsoft_timepicker { width: 58px; float: left; text-align: center; margin-left: 8px; margin-top: 0; } .xdsoft_datetimepicker .xdsoft_datepicker.active+.xdsoft_timepicker { margin-top: 8px; margin-bottom: 3px } .xdsoft_datetimepicker .xdsoft_mounthpicker { position: relative; text-align: center; } .xdsoft_datetimepicker .xdsoft_label i, .xdsoft_datetimepicker .xdsoft_prev, .xdsoft_datetimepicker .xdsoft_next, .xdsoft_datetimepicker .xdsoft_today_button { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAAAeCAYAAADaW7vzAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6Q0NBRjI1NjM0M0UwMTFFNDk4NkFGMzJFQkQzQjEwRUIiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6Q0NBRjI1NjQ0M0UwMTFFNDk4NkFGMzJFQkQzQjEwRUIiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpDQ0FGMjU2MTQzRTAxMUU0OTg2QUYzMkVCRDNCMTBFQiIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpDQ0FGMjU2MjQzRTAxMUU0OTg2QUYzMkVCRDNCMTBFQiIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PoNEP54AAAIOSURBVHja7Jq9TsMwEMcxrZD4WpBYeKUCe+kTMCACHZh4BFfHO/AAIHZGFhYkBBsSEqxsLCAgXKhbXYOTxh9pfJVP+qutnZ5s/5Lz2Y5I03QhWji2GIcgAokWgfCxNvcOCCGKqiSqhUp0laHOne05vdEyGMfkdxJDVjgwDlEQgYQBgx+ULJaWSXXS6r/ER5FBVR8VfGftTKcITNs+a1XpcFoExREIDF14AVIFxgQUS+h520cdud6wNkC0UBw6BCO/HoCYwBhD8QCkQ/x1mwDyD4plh4D6DDV0TAGyo4HcawLIBBSLDkHeH0Mg2yVP3l4TQMZQDDsEOl/MgHQqhMNuE0D+oBh0CIr8MAKyazBH9WyBuKxDWgbXfjNf32TZ1KWm/Ap1oSk/R53UtQ5xTh3LUlMmT8gt6g51Q9p+SobxgJQ/qmsfZhWywGFSl0yBjCLJCMgXail3b7+rumdVJ2YRss4cN+r6qAHDkPWjPjdJCF4n9RmAD/V9A/Wp4NQassDjwlB6XBiCxcJQWmZZb8THFilfy/lfrTvLghq2TqTHrRMTKNJ0sIhdo15RT+RpyWwFdY96UZ/LdQKBGjcXpcc1AlSFEfLmouD+1knuxBDUVrvOBmoOC/rEcN7OQxKVeJTCiAdUzUJhA2Oez9QTkp72OTVcxDcXY8iKNkxGAJXmJCOQwOa6dhyXsOa6XwEGAKdeb5ET3rQdAAAAAElFTkSuQmCC); } .xdsoft_datetimepicker .xdsoft_label i { opacity: 0.5; background-position: -92px -19px; display: inline-block; width: 9px; height: 20px; vertical-align: middle; } .xdsoft_datetimepicker .xdsoft_prev { float: left; background-position: -20px 0; } .xdsoft_datetimepicker .xdsoft_today_button { float: left; background-position: -70px 0; margin-left: 5px; } .xdsoft_datetimepicker .xdsoft_next { float: right; background-position: 0 0; } .xdsoft_datetimepicker .xdsoft_next, .xdsoft_datetimepicker .xdsoft_prev , .xdsoft_datetimepicker .xdsoft_today_button { background-color: transparent; background-repeat: no-repeat; border: 0 none; cursor: pointer; display: block; height: 30px; opacity: 0.5; -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=50)"; outline: medium none; overflow: hidden; padding: 0; position: relative; text-indent: 100%; white-space: nowrap; width: 20px; min-width: 0; } .xdsoft_datetimepicker .xdsoft_timepicker .xdsoft_prev, .xdsoft_datetimepicker .xdsoft_timepicker .xdsoft_next { float: none; background-position: -40px -15px; height: 15px; width: 30px; display: block; margin-left: 14px; margin-top: 7px; } .xdsoft_datetimepicker .xdsoft_timepicker .xdsoft_prev { background-position: -40px 0; margin-bottom: 7px; margin-top: 0; } .xdsoft_datetimepicker .xdsoft_timepicker .xdsoft_time_box { height: 151px; overflow: hidden; border-bottom: 1px solid #ddd; } .xdsoft_datetimepicker .xdsoft_timepicker .xdsoft_time_box >div >div { background: #f5f5f5; border-top: 1px solid #ddd; color: #666; font-size: 12px; text-align: center; border-collapse: collapse; cursor: pointer; border-bottom-width: 0; height: 25px; line-height: 25px; } .xdsoft_datetimepicker .xdsoft_timepicker .xdsoft_time_box >div > div:first-child { border-top-width: 0; } .xdsoft_datetimepicker .xdsoft_today_button:hover, .xdsoft_datetimepicker .xdsoft_next:hover, .xdsoft_datetimepicker .xdsoft_prev:hover { opacity: 1; -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=100)"; } .xdsoft_datetimepicker .xdsoft_label { display: inline; position: relative; z-index: 9999; margin: 0; padding: 5px 3px; font-size: 14px; line-height: 20px; font-weight: bold; background-color: #fff; float: left; width: 182px; text-align: center; cursor: pointer; } .xdsoft_datetimepicker .xdsoft_label:hover>span { text-decoration: underline; } .xdsoft_datetimepicker .xdsoft_label:hover i { opacity: 1.0; } .xdsoft_datetimepicker .xdsoft_label > .xdsoft_select { border: 1px solid #ccc; position: absolute; right: 0; top: 30px; z-index: 101; display: none; background: #fff; max-height: 160px; overflow-y: hidden; } .xdsoft_datetimepicker .xdsoft_label > .xdsoft_select.xdsoft_monthselect{ right: -7px } .xdsoft_datetimepicker .xdsoft_label > .xdsoft_select.xdsoft_yearselect{ right: 2px } .xdsoft_datetimepicker .xdsoft_label > .xdsoft_select > div > .xdsoft_option:hover { color: #fff; background: #ff8000; } .xdsoft_datetimepicker .xdsoft_label > .xdsoft_select > div > .xdsoft_option { padding: 2px 10px 2px 5px; text-decoration: none !important; } .xdsoft_datetimepicker .xdsoft_label > .xdsoft_select > div > .xdsoft_option.xdsoft_current { background: #33aaff; box-shadow: #178fe5 0 1px 3px 0 inset; color: #fff; font-weight: 700; } .xdsoft_datetimepicker .xdsoft_month { width: 100px; text-align: right; } .xdsoft_datetimepicker .xdsoft_calendar { clear: both; } .xdsoft_datetimepicker .xdsoft_year{ width: 48px; margin-left: 5px; } .xdsoft_datetimepicker .xdsoft_calendar table { border-collapse: collapse; width: 100%; } .xdsoft_datetimepicker .xdsoft_calendar td > div { padding-right: 5px; } .xdsoft_datetimepicker .xdsoft_calendar th { height: 25px; } .xdsoft_datetimepicker .xdsoft_calendar td,.xdsoft_datetimepicker .xdsoft_calendar th { width: 14.2857142%; background: #f5f5f5; border: 1px solid #ddd; color: #666; font-size: 12px; text-align: right; vertical-align: middle; padding: 0; border-collapse: collapse; cursor: pointer; height: 25px; } .xdsoft_datetimepicker.xdsoft_showweeks .xdsoft_calendar td,.xdsoft_datetimepicker.xdsoft_showweeks .xdsoft_calendar th { width: 12.5%; } .xdsoft_datetimepicker .xdsoft_calendar th { background: #f1f1f1; } .xdsoft_datetimepicker .xdsoft_calendar td.xdsoft_today { color: #33aaff; } .xdsoft_datetimepicker .xdsoft_calendar td.xdsoft_highlighted_default { background: #ffe9d2; box-shadow: #ffb871 0 1px 4px 0 inset; color: #000; } .xdsoft_datetimepicker .xdsoft_calendar td.xdsoft_highlighted_mint { background: #c1ffc9; box-shadow: #00dd1c 0 1px 4px 0 inset; color: #000; } .xdsoft_datetimepicker .xdsoft_calendar td.xdsoft_default, .xdsoft_datetimepicker .xdsoft_calendar td.xdsoft_current, .xdsoft_datetimepicker .xdsoft_timepicker .xdsoft_time_box >div >div.xdsoft_current { background: #33aaff; box-shadow: #178fe5 0 1px 3px 0 inset; color: #fff; font-weight: 700; } .xdsoft_datetimepicker .xdsoft_calendar td.xdsoft_other_month, .xdsoft_datetimepicker .xdsoft_calendar td.xdsoft_disabled, .xdsoft_datetimepicker .xdsoft_time_box >div >div.xdsoft_disabled { opacity: 0.5; -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=50)"; cursor: default; } .xdsoft_datetimepicker .xdsoft_calendar td.xdsoft_other_month.xdsoft_disabled { opacity: 0.2; -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=20)"; } .xdsoft_datetimepicker .xdsoft_calendar td:hover, .xdsoft_datetimepicker .xdsoft_timepicker .xdsoft_time_box >div >div:hover { color: #fff !important; background: #ff8000 !important; box-shadow: none !important; } .xdsoft_datetimepicker .xdsoft_calendar td.xdsoft_current.xdsoft_disabled:hover, .xdsoft_datetimepicker .xdsoft_timepicker .xdsoft_time_box>div>div.xdsoft_current.xdsoft_disabled:hover { background: #33aaff !important; box-shadow: #178fe5 0 1px 3px 0 inset !important; color: #fff !important; } .xdsoft_datetimepicker .xdsoft_calendar td.xdsoft_disabled:hover, .xdsoft_datetimepicker .xdsoft_timepicker .xdsoft_time_box >div >div.xdsoft_disabled:hover { color: inherit !important; background: inherit !important; box-shadow: inherit !important; } .xdsoft_datetimepicker .xdsoft_calendar th { font-weight: 700; text-align: center; color: #999; cursor: default; } .xdsoft_datetimepicker .xdsoft_copyright { color: #ccc !important; font-size: 10px; clear: both; float: none; margin-left: 8px; } .xdsoft_datetimepicker .xdsoft_copyright a { color: #eee !important } .xdsoft_datetimepicker .xdsoft_copyright a:hover { color: #aaa !important } .xdsoft_time_box { position: relative; border: 1px solid #ccc; } .xdsoft_scrollbar >.xdsoft_scroller { background: #ccc !important; height: 20px; border-radius: 3px; } .xdsoft_scrollbar { position: absolute; width: 7px; right: 0; top: 0; bottom: 0; cursor: pointer; } .xdsoft_scroller_box { position: relative; } .xdsoft_datetimepicker.xdsoft_dark { box-shadow: 0 5px 15px -5px rgba(255, 255, 255, 0.506); background: #000; border-bottom: 1px solid #444; border-left: 1px solid #333; border-right: 1px solid #333; border-top: 1px solid #333; color: #ccc; } .xdsoft_datetimepicker.xdsoft_dark .xdsoft_timepicker .xdsoft_time_box { border-bottom: 1px solid #222; } .xdsoft_datetimepicker.xdsoft_dark .xdsoft_timepicker .xdsoft_time_box >div >div { background: #0a0a0a; border-top: 1px solid #222; color: #999; } .xdsoft_datetimepicker.xdsoft_dark .xdsoft_label { background-color: #000; } .xdsoft_datetimepicker.xdsoft_dark .xdsoft_label > .xdsoft_select { border: 1px solid #333; background: #000; } .xdsoft_datetimepicker.xdsoft_dark .xdsoft_label > .xdsoft_select > div > .xdsoft_option:hover { color: #000; background: #007fff; } .xdsoft_datetimepicker.xdsoft_dark .xdsoft_label > .xdsoft_select > div > .xdsoft_option.xdsoft_current { background: #cc5500; box-shadow: #b03e00 0 1px 3px 0 inset; color: #000; } .xdsoft_datetimepicker.xdsoft_dark .xdsoft_label i, .xdsoft_datetimepicker.xdsoft_dark .xdsoft_prev, .xdsoft_datetimepicker.xdsoft_dark .xdsoft_next, .xdsoft_datetimepicker.xdsoft_dark .xdsoft_today_button { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAAAeCAYAAADaW7vzAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6QUExQUUzOTA0M0UyMTFFNDlBM0FFQTJENTExRDVBODYiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6QUExQUUzOTE0M0UyMTFFNDlBM0FFQTJENTExRDVBODYiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpBQTFBRTM4RTQzRTIxMUU0OUEzQUVBMkQ1MTFENUE4NiIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpBQTFBRTM4RjQzRTIxMUU0OUEzQUVBMkQ1MTFENUE4NiIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pp0VxGEAAAIASURBVHja7JrNSgMxEMebtgh+3MSLr1T1Xn2CHoSKB08+QmR8Bx9A8e7RixdB9CKCoNdexIugxFlJa7rNZneTbLIpM/CnNLsdMvNjM8l0mRCiQ9Ye61IKCAgZAUnH+mU3MMZaHYChBnJUDzWOFZdVfc5+ZFLbrWDeXPwbxIqrLLfaeS0hEBVGIRQCEiZoHQwtlGSByCCdYBl8g8egTTAWoKQMRBRBcZxYlhzhKegqMOageErsCHVkk3hXIFooDgHB1KkHIHVgzKB4ADJQ/A1jAFmAYhkQqA5TOBtocrKrgXwQA8gcFIuAIO8sQSA7hidvPwaQGZSaAYHOUWJABhWWw2EMIH9QagQERU4SArJXo0ZZL18uvaxejXt/Em8xjVBXmvFr1KVm/AJ10tRe2XnraNqaJvKE3KHuUbfK1E+VHB0q40/y3sdQSxY4FHWeKJCunP8UyDdqJZenT3ntVV5jIYCAh20vT7ioP8tpf6E2lfEMwERe+whV1MHjwZB7PBiCxcGQWwKZKD62lfGNnP/1poFAA60T7rF1UgcKd2id3KDeUS+oLWV8DfWAepOfq00CgQabi9zjcgJVYVD7PVzQUAUGAQkbNJTBICDhgwYTjDYD6XeW08ZKh+A4pYkzenOxXUbvZcWz7E8ykRMnIHGX1XPl+1m2vPYpL+2qdb8CDAARlKFEz/ZVkAAAAABJRU5ErkJggg==); } .xdsoft_datetimepicker.xdsoft_dark .xdsoft_calendar td, .xdsoft_datetimepicker.xdsoft_dark .xdsoft_calendar th { background: #0a0a0a; border: 1px solid #222; color: #999; } .xdsoft_datetimepicker.xdsoft_dark .xdsoft_calendar th { background: #0e0e0e; } .xdsoft_datetimepicker.xdsoft_dark .xdsoft_calendar td.xdsoft_today { color: #cc5500; } .xdsoft_datetimepicker.xdsoft_dark .xdsoft_calendar td.xdsoft_highlighted_default { background: #ffe9d2; box-shadow: #ffb871 0 1px 4px 0 inset; color:#000; } .xdsoft_datetimepicker.xdsoft_dark .xdsoft_calendar td.xdsoft_highlighted_mint { background: #c1ffc9; box-shadow: #00dd1c 0 1px 4px 0 inset; color:#000; } .xdsoft_datetimepicker.xdsoft_dark .xdsoft_calendar td.xdsoft_default, .xdsoft_datetimepicker.xdsoft_dark .xdsoft_calendar td.xdsoft_current, .xdsoft_datetimepicker.xdsoft_dark .xdsoft_timepicker .xdsoft_time_box >div >div.xdsoft_current { background: #cc5500; box-shadow: #b03e00 0 1px 3px 0 inset; color: #000; } .xdsoft_datetimepicker.xdsoft_dark .xdsoft_calendar td:hover, .xdsoft_datetimepicker.xdsoft_dark .xdsoft_timepicker .xdsoft_time_box >div >div:hover { color: #000 !important; background: #007fff !important; } .xdsoft_datetimepicker.xdsoft_dark .xdsoft_calendar th { color: #666; } .xdsoft_datetimepicker.xdsoft_dark .xdsoft_copyright { color: #333 !important } .xdsoft_datetimepicker.xdsoft_dark .xdsoft_copyright a { color: #111 !important } .xdsoft_datetimepicker.xdsoft_dark .xdsoft_copyright a:hover { color: #555 !important } .xdsoft_dark .xdsoft_time_box { border: 1px solid #333; } .xdsoft_dark .xdsoft_scrollbar >.xdsoft_scroller { background: #333 !important; } .xdsoft_datetimepicker .xdsoft_save_selected { display: block; border: 1px solid #dddddd !important; margin-top: 5px; width: 100%; color: #454551; font-size: 13px; } .xdsoft_datetimepicker .blue-gradient-button { font-family: "museo-sans", "Book Antiqua", sans-serif; font-size: 12px; font-weight: 300; color: #82878c; height: 28px; position: relative; padding: 4px 17px 4px 33px; border: 1px solid #d7d8da; background: -moz-linear-gradient(top, #fff 0%, #f4f8fa 73%); /* FF3.6+ */ background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #fff), color-stop(73%, #f4f8fa)); /* Chrome,Safari4+ */ background: -webkit-linear-gradient(top, #fff 0%, #f4f8fa 73%); /* Chrome10+,Safari5.1+ */ background: -o-linear-gradient(top, #fff 0%, #f4f8fa 73%); /* Opera 11.10+ */ background: -ms-linear-gradient(top, #fff 0%, #f4f8fa 73%); /* IE10+ */ background: linear-gradient(to bottom, #fff 0%, #f4f8fa 73%); /* W3C */ filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#fff', endColorstr='#f4f8fa',GradientType=0 ); /* IE6-9 */ } .xdsoft_datetimepicker .blue-gradient-button:hover, .xdsoft_datetimepicker .blue-gradient-button:focus, .xdsoft_datetimepicker .blue-gradient-button:hover span, .xdsoft_datetimepicker .blue-gradient-button:focus span { color: #454551; background: -moz-linear-gradient(top, #f4f8fa 0%, #FFF 73%); /* FF3.6+ */ background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #f4f8fa), color-stop(73%, #FFF)); /* Chrome,Safari4+ */ background: -webkit-linear-gradient(top, #f4f8fa 0%, #FFF 73%); /* Chrome10+,Safari5.1+ */ background: -o-linear-gradient(top, #f4f8fa 0%, #FFF 73%); /* Opera 11.10+ */ background: -ms-linear-gradient(top, #f4f8fa 0%, #FFF 73%); /* IE10+ */ background: linear-gradient(to bottom, #f4f8fa 0%, #FFF 73%); /* W3C */ filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#f4f8fa', endColorstr='#FFF',GradientType=0 ); /* IE6-9 */ } ================================================ FILE: silk/static/silk/lib/jquery.datetimepicker.js ================================================ /** * @preserve jQuery DateTimePicker plugin v2.4.3 * @homepage http://xdsoft.net/jqplugins/datetimepicker/ * (c) 2014, Chupurnov Valeriy. */ /*global document,window,jQuery,setTimeout,clearTimeout,HighlightedDate,getCurrentValue*/ (function ($) { 'use strict'; var default_options = { i18n: { ar: { // Arabic months: [ "كانون الثاني", "شباط", "آذار", "نيسان", "مايو", "حزيران", "تموز", "آب", "أيلول", "تشرين الأول", "تشرين الثاني", "كانون الأول" ], dayOfWeek: [ "ن", "ث", "ع", "خ", "ج", "س", "ح" ] }, ro: { // Romanian months: [ "ianuarie", "februarie", "martie", "aprilie", "mai", "iunie", "iulie", "august", "septembrie", "octombrie", "noiembrie", "decembrie" ], dayOfWeek: [ "l", "ma", "mi", "j", "v", "s", "d" ] }, id: { // Indonesian months: [ "Januari", "Februari", "Maret", "April", "Mei", "Juni", "Juli", "Agustus", "September", "Oktober", "November", "Desember" ], dayOfWeek: [ "Min", "Sen", "Sel", "Rab", "Kam", "Jum", "Sab" ] }, bg: { // Bulgarian months: [ "Януари", "Февруари", "Март", "Април", "Май", "Юни", "Юли", "Август", "Септември", "Октомври", "Ноември", "Декември" ], dayOfWeek: [ "Нд", "Пн", "Вт", "Ср", "Чт", "Пт", "Сб" ] }, fa: { // Persian/Farsi months: [ 'فروردین', 'اردیبهشت', 'خرداد', 'تیر', 'مرداد', 'شهریور', 'مهر', 'آبان', 'آذر', 'دی', 'بهمن', 'اسفند' ], dayOfWeek: [ 'یکشنبه', 'دوشنبه', 'سه شنبه', 'چهارشنبه', 'پنجشنبه', 'جمعه', 'شنبه' ] }, ru: { // Russian months: [ 'Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь', 'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь' ], dayOfWeek: [ "Вск", "Пн", "Вт", "Ср", "Чт", "Пт", "Сб" ] }, uk: { // Ukrainian months: [ 'Січень', 'Лютий', 'Березень', 'Квітень', 'Травень', 'Червень', 'Липень', 'Серпень', 'Вересень', 'Жовтень', 'Листопад', 'Грудень' ], dayOfWeek: [ "Ндл", "Пнд", "Втр", "Срд", "Чтв", "Птн", "Сбт" ] }, en: { // English months: [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ], dayOfWeek: [ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" ] }, el: { // Ελληνικά months: [ "Ιανουάριος", "Φεβρουάριος", "Μάρτιος", "Απρίλιος", "Μάιος", "Ιούνιος", "Ιούλιος", "Αύγουστος", "Σεπτέμβριος", "Οκτώβριος", "Νοέμβριος", "Δεκέμβριος" ], dayOfWeek: [ "Κυρ", "Δευ", "Τρι", "Τετ", "Πεμ", "Παρ", "Σαβ" ] }, de: { // German months: [ 'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember' ], dayOfWeek: [ "So", "Mo", "Di", "Mi", "Do", "Fr", "Sa" ] }, nl: { // Dutch months: [ "januari", "februari", "maart", "april", "mei", "juni", "juli", "augustus", "september", "oktober", "november", "december" ], dayOfWeek: [ "zo", "ma", "di", "wo", "do", "vr", "za" ] }, tr: { // Turkish months: [ "Ocak", "Şubat", "Mart", "Nisan", "Mayıs", "Haziran", "Temmuz", "Ağustos", "Eylül", "Ekim", "Kasım", "Aralık" ], dayOfWeek: [ "Paz", "Pts", "Sal", "Çar", "Per", "Cum", "Cts" ] }, fr: { //French months: [ "Janvier", "Février", "Mars", "Avril", "Mai", "Juin", "Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre" ], dayOfWeek: [ "Dim", "Lun", "Mar", "Mer", "Jeu", "Ven", "Sam" ] }, es: { // Spanish months: [ "Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre" ], dayOfWeek: [ "Dom", "Lun", "Mar", "Mié", "Jue", "Vie", "Sáb" ] }, th: { // Thai months: [ 'มกราคม', 'กุมภาพันธ์', 'มีนาคม', 'เมษายน', 'พฤษภาคม', 'มิถุนายน', 'กรกฎาคม', 'สิงหาคม', 'กันยายน', 'ตุลาคม', 'พฤศจิกายน', 'ธันวาคม' ], dayOfWeek: [ 'อา.', 'จ.', 'อ.', 'พ.', 'พฤ.', 'ศ.', 'ส.' ] }, pl: { // Polish months: [ "styczeń", "luty", "marzec", "kwiecień", "maj", "czerwiec", "lipiec", "sierpień", "wrzesień", "październik", "listopad", "grudzień" ], dayOfWeek: [ "nd", "pn", "wt", "śr", "cz", "pt", "sb" ] }, pt: { // Portuguese months: [ "Janeiro", "Fevereiro", "Março", "Abril", "Maio", "Junho", "Julho", "Agosto", "Setembro", "Outubro", "Novembro", "Dezembro" ], dayOfWeek: [ "Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "Sab" ] }, ch: { // Simplified Chinese months: [ "一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月" ], dayOfWeek: [ "日", "一", "二", "三", "四", "五", "六" ] }, se: { // Swedish months: [ "Januari", "Februari", "Mars", "April", "Maj", "Juni", "Juli", "Augusti", "September", "Oktober", "November", "December" ], dayOfWeek: [ "Sön", "Mån", "Tis", "Ons", "Tor", "Fre", "Lör" ] }, kr: { // Korean months: [ "1월", "2월", "3월", "4월", "5월", "6월", "7월", "8월", "9월", "10월", "11월", "12월" ], dayOfWeek: [ "일", "월", "화", "수", "목", "금", "토" ] }, it: { // Italian months: [ "Gennaio", "Febbraio", "Marzo", "Aprile", "Maggio", "Giugno", "Luglio", "Agosto", "Settembre", "Ottobre", "Novembre", "Dicembre" ], dayOfWeek: [ "Dom", "Lun", "Mar", "Mer", "Gio", "Ven", "Sab" ] }, da: { // Dansk months: [ "January", "Februar", "Marts", "April", "Maj", "Juni", "July", "August", "September", "Oktober", "November", "December" ], dayOfWeek: [ "Søn", "Man", "Tir", "Ons", "Tor", "Fre", "Lør" ] }, no: { // Norwegian months: [ "Januar", "Februar", "Mars", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Desember" ], dayOfWeek: [ "Søn", "Man", "Tir", "Ons", "Tor", "Fre", "Lør" ] }, ja: { // Japanese months: [ "1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月" ], dayOfWeek: [ "日", "月", "火", "水", "木", "金", "土" ] }, vi: { // Vietnamese months: [ "Tháng 1", "Tháng 2", "Tháng 3", "Tháng 4", "Tháng 5", "Tháng 6", "Tháng 7", "Tháng 8", "Tháng 9", "Tháng 10", "Tháng 11", "Tháng 12" ], dayOfWeek: [ "CN", "T2", "T3", "T4", "T5", "T6", "T7" ] }, sl: { // Slovenščina months: [ "Januar", "Februar", "Marec", "April", "Maj", "Junij", "Julij", "Avgust", "September", "Oktober", "November", "December" ], dayOfWeek: [ "Ned", "Pon", "Tor", "Sre", "Čet", "Pet", "Sob" ] }, cs: { // Čeština months: [ "Leden", "Únor", "Březen", "Duben", "Květen", "Červen", "Červenec", "Srpen", "Září", "Říjen", "Listopad", "Prosinec" ], dayOfWeek: [ "Ne", "Po", "Út", "St", "Čt", "Pá", "So" ] }, hu: { // Hungarian months: [ "Január", "Február", "Március", "Április", "Május", "Június", "Július", "Augusztus", "Szeptember", "Október", "November", "December" ], dayOfWeek: [ "Va", "Hé", "Ke", "Sze", "Cs", "Pé", "Szo" ] }, az: { //Azerbaijanian (Azeri) months: [ "Yanvar", "Fevral", "Mart", "Aprel", "May", "Iyun", "Iyul", "Avqust", "Sentyabr", "Oktyabr", "Noyabr", "Dekabr" ], dayOfWeek: [ "B", "Be", "Ça", "Ç", "Ca", "C", "Ş" ] }, bs: { //Bosanski months: [ "Januar", "Februar", "Mart", "April", "Maj", "Jun", "Jul", "Avgust", "Septembar", "Oktobar", "Novembar", "Decembar" ], dayOfWeek: [ "Ned", "Pon", "Uto", "Sri", "Čet", "Pet", "Sub" ] }, ca: { //Català months: [ "Gener", "Febrer", "Març", "Abril", "Maig", "Juny", "Juliol", "Agost", "Setembre", "Octubre", "Novembre", "Desembre" ], dayOfWeek: [ "Dg", "Dl", "Dt", "Dc", "Dj", "Dv", "Ds" ] }, 'en-GB': { //English (British) months: [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ], dayOfWeek: [ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" ] }, et: { //"Eesti" months: [ "Jaanuar", "Veebruar", "Märts", "Aprill", "Mai", "Juuni", "Juuli", "August", "September", "Oktoober", "November", "Detsember" ], dayOfWeek: [ "P", "E", "T", "K", "N", "R", "L" ] }, eu: { //Euskara months: [ "Urtarrila", "Otsaila", "Martxoa", "Apirila", "Maiatza", "Ekaina", "Uztaila", "Abuztua", "Iraila", "Urria", "Azaroa", "Abendua" ], dayOfWeek: [ "Ig.", "Al.", "Ar.", "Az.", "Og.", "Or.", "La." ] }, fi: { //Finnish (Suomi) months: [ "Tammikuu", "Helmikuu", "Maaliskuu", "Huhtikuu", "Toukokuu", "Kesäkuu", "Heinäkuu", "Elokuu", "Syyskuu", "Lokakuu", "Marraskuu", "Joulukuu" ], dayOfWeek: [ "Su", "Ma", "Ti", "Ke", "To", "Pe", "La" ] }, gl: { //Galego months: [ "Xan", "Feb", "Maz", "Abr", "Mai", "Xun", "Xul", "Ago", "Set", "Out", "Nov", "Dec" ], dayOfWeek: [ "Dom", "Lun", "Mar", "Mer", "Xov", "Ven", "Sab" ] }, hr: { //Hrvatski months: [ "Siječanj", "Veljača", "Ožujak", "Travanj", "Svibanj", "Lipanj", "Srpanj", "Kolovoz", "Rujan", "Listopad", "Studeni", "Prosinac" ], dayOfWeek: [ "Ned", "Pon", "Uto", "Sri", "Čet", "Pet", "Sub" ] }, ko: { //Korean (한국어) months: [ "1월", "2월", "3월", "4월", "5월", "6월", "7월", "8월", "9월", "10월", "11월", "12월" ], dayOfWeek: [ "일", "월", "화", "수", "목", "금", "토" ] }, lt: { //Lithuanian (lietuvių) months: [ "Sausio", "Vasario", "Kovo", "Balandžio", "Gegužės", "Birželio", "Liepos", "Rugpjūčio", "Rugsėjo", "Spalio", "Lapkričio", "Gruodžio" ], dayOfWeek: [ "Sek", "Pir", "Ant", "Tre", "Ket", "Pen", "Šeš" ] }, lv: { //Latvian (Latviešu) months: [ "Janvāris", "Februāris", "Marts", "Aprīlis ", "Maijs", "Jūnijs", "Jūlijs", "Augusts", "Septembris", "Oktobris", "Novembris", "Decembris" ], dayOfWeek: [ "Sv", "Pr", "Ot", "Tr", "Ct", "Pk", "St" ] }, mk: { //Macedonian (Македонски) months: [ "јануари", "февруари", "март", "април", "мај", "јуни", "јули", "август", "септември", "октомври", "ноември", "декември" ], dayOfWeek: [ "нед", "пон", "вто", "сре", "чет", "пет", "саб" ] }, mn: { //Mongolian (Монгол) months: [ "1-р сар", "2-р сар", "3-р сар", "4-р сар", "5-р сар", "6-р сар", "7-р сар", "8-р сар", "9-р сар", "10-р сар", "11-р сар", "12-р сар" ], dayOfWeek: [ "Дав", "Мяг", "Лха", "Пүр", "Бсн", "Бям", "Ням" ] }, 'pt-BR': { //Português(Brasil) months: [ "Janeiro", "Fevereiro", "Março", "Abril", "Maio", "Junho", "Julho", "Agosto", "Setembro", "Outubro", "Novembro", "Dezembro" ], dayOfWeek: [ "Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "Sáb" ] }, sk: { //Slovenčina months: [ "Január", "Február", "Marec", "Apríl", "Máj", "Jún", "Júl", "August", "September", "Október", "November", "December" ], dayOfWeek: [ "Ne", "Po", "Ut", "St", "Št", "Pi", "So" ] }, sq: { //Albanian (Shqip) months: [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ], dayOfWeek: [ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" ] }, 'sr-YU': { //Serbian (Srpski) months: [ "Januar", "Februar", "Mart", "April", "Maj", "Jun", "Jul", "Avgust", "Septembar", "Oktobar", "Novembar", "Decembar" ], dayOfWeek: [ "Ned", "Pon", "Uto", "Sre", "čet", "Pet", "Sub" ] }, sr: { //Serbian Cyrillic (Српски) months: [ "јануар", "фебруар", "март", "април", "мај", "јун", "јул", "август", "септембар", "октобар", "новембар", "децембар" ], dayOfWeek: [ "нед", "пон", "уто", "сре", "чет", "пет", "суб" ] }, sv: { //Svenska months: [ "Januari", "Februari", "Mars", "April", "Maj", "Juni", "Juli", "Augusti", "September", "Oktober", "November", "December" ], dayOfWeek: [ "Sön", "Mån", "Tis", "Ons", "Tor", "Fre", "Lör" ] }, 'zh-TW': { //Traditional Chinese (繁體中文) months: [ "一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月" ], dayOfWeek: [ "日", "一", "二", "三", "四", "五", "六" ] }, zh: { //Simplified Chinese (简体中文) months: [ "一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月" ], dayOfWeek: [ "日", "一", "二", "三", "四", "五", "六" ] }, he: { //Hebrew (עברית) months: [ 'ינואר', 'פברואר', 'מרץ', 'אפריל', 'מאי', 'יוני', 'יולי', 'אוגוסט', 'ספטמבר', 'אוקטובר', 'נובמבר', 'דצמבר' ], dayOfWeek: [ 'א\'', 'ב\'', 'ג\'', 'ד\'', 'ה\'', 'ו\'', 'שבת' ] }, hy: { // Armenian months: [ "Հունվար", "Փետրվար", "Մարտ", "Ապրիլ", "Մայիս", "Հունիս", "Հուլիս", "Օգոստոս", "Սեպտեմբեր", "Հոկտեմբեր", "Նոյեմբեր", "Դեկտեմբեր" ], dayOfWeek: [ "Կի", "Երկ", "Երք", "Չոր", "Հնգ", "Ուրբ", "Շբթ" ] }, kg: { // Kyrgyz months: [ 'Үчтүн айы', 'Бирдин айы', 'Жалган Куран', 'Чын Куран', 'Бугу', 'Кулжа', 'Теке', 'Баш Оона', 'Аяк Оона', 'Тогуздун айы', 'Жетинин айы', 'Бештин айы' ], dayOfWeek: [ "Жек", "Дүй", "Шей", "Шар", "Бей", "Жум", "Ише" ] } }, value: '', lang: 'en', format: 'Y/m/d H:i', formatTime: 'H:i', formatDate: 'Y/m/d', startDate: false, // new Date(), '1986/12/08', '-1970/01/05','-1970/01/05', step: 60, monthChangeSpinner: true, closeOnDateSelect: false, closeOnTimeSelect: true, closeOnWithoutClick: true, closeOnInputClick: true, timepicker: true, datepicker: true, weeks: false, defaultTime: false, // use formatTime format (ex. '10:00' for formatTime: 'H:i') defaultDate: false, // use formatDate format (ex new Date() or '1986/12/08' or '-1970/01/05' or '-1970/01/05') minDate: false, maxDate: false, minTime: false, maxTime: false, allowTimes: [], opened: false, initTime: true, inline: false, theme: '', onSelectDate: function () {}, onSelectTime: function () {}, onChangeMonth: function () {}, onChangeYear: function () {}, onChangeDateTime: function () {}, onShow: function () {}, onClose: function () {}, onGenerate: function () {}, withoutCopyright: true, inverseButton: false, hours12: false, next: 'xdsoft_next', prev : 'xdsoft_prev', dayOfWeekStart: 0, parentID: 'body', timeHeightInTimePicker: 25, timepickerScrollbar: true, todayButton: true, prevButton: true, nextButton: true, defaultSelect: true, scrollMonth: true, scrollTime: true, scrollInput: true, lazyInit: false, mask: false, validateOnBlur: true, allowBlank: true, yearStart: 1950, yearEnd: 2050, monthStart: 0, monthEnd: 11, style: '', id: '', fixed: false, roundTime: 'round', // ceil, floor className: '', weekends: [], highlightedDates: [], highlightedPeriods: [], disabledDates : [], yearOffset: 0, beforeShowDay: null, enterLikeTab: true, showApplyButton: false }; // fix for ie8 if (!Array.prototype.indexOf) { Array.prototype.indexOf = function (obj, start) { var i, j; for (i = (start || 0), j = this.length; i < j; i += 1) { if (this[i] === obj) { return i; } } return -1; }; } Date.prototype.countDaysInMonth = function () { return new Date(this.getFullYear(), this.getMonth() + 1, 0).getDate(); }; $.fn.xdsoftScroller = function (percent) { return this.each(function () { var timeboxparent = $(this), pointerEventToXY = function (e) { var out = {x: 0, y: 0}, touch; if (e.type === 'touchstart' || e.type === 'touchmove' || e.type === 'touchend' || e.type === 'touchcancel') { touch = e.originalEvent.touches[0] || e.originalEvent.changedTouches[0]; out.x = touch.clientX; out.y = touch.clientY; } else if (e.type === 'mousedown' || e.type === 'mouseup' || e.type === 'mousemove' || e.type === 'mouseover' || e.type === 'mouseout' || e.type === 'mouseenter' || e.type === 'mouseleave') { out.x = e.clientX; out.y = e.clientY; } return out; }, move = 0, timebox, parentHeight, height, scrollbar, scroller, maximumOffset = 100, start = false, startY = 0, startTop = 0, h1 = 0, touchStart = false, startTopScroll = 0, calcOffset = function () {}; if (percent === 'hide') { timeboxparent.find('.xdsoft_scrollbar').hide(); return; } if (!$(this).hasClass('xdsoft_scroller_box')) { timebox = timeboxparent.children().eq(0); parentHeight = timeboxparent[0].clientHeight; height = timebox[0].offsetHeight; scrollbar = $('
'); scroller = $('
'); scrollbar.append(scroller); timeboxparent.addClass('xdsoft_scroller_box').append(scrollbar); calcOffset = function calcOffset(event) { var offset = pointerEventToXY(event).y - startY + startTopScroll; if (offset < 0) { offset = 0; } if (offset + scroller[0].offsetHeight > h1) { offset = h1 - scroller[0].offsetHeight; } timeboxparent.trigger('scroll_element.xdsoft_scroller', [maximumOffset ? offset / maximumOffset : 0]); }; scroller .on('touchstart.xdsoft_scroller mousedown.xdsoft_scroller', function (event) { if (!parentHeight) { timeboxparent.trigger('resize_scroll.xdsoft_scroller', [percent]); } startY = pointerEventToXY(event).y; startTopScroll = parseInt(scroller.css('margin-top'), 10); h1 = scrollbar[0].offsetHeight; if (event.type === 'mousedown') { if (document) { $(document.body).addClass('xdsoft_noselect'); } $([document.body, window]).on('mouseup.xdsoft_scroller', function arguments_callee() { $([document.body, window]).off('mouseup.xdsoft_scroller', arguments_callee) .off('mousemove.xdsoft_scroller', calcOffset) .removeClass('xdsoft_noselect'); }); $(document.body).on('mousemove.xdsoft_scroller', calcOffset); } else { touchStart = true; event.stopPropagation(); event.preventDefault(); } }) .on('touchmove', function (event) { if (touchStart) { event.preventDefault(); calcOffset(event); } }) .on('touchend touchcancel', function (event) { touchStart = false; startTopScroll = 0; }); timeboxparent .on('scroll_element.xdsoft_scroller', function (event, percentage) { if (!parentHeight) { timeboxparent.trigger('resize_scroll.xdsoft_scroller', [percentage, true]); } percentage = percentage > 1 ? 1 : (percentage < 0 || isNaN(percentage)) ? 0 : percentage; scroller.css('margin-top', maximumOffset * percentage); setTimeout(function () { timebox.css('marginTop', -parseInt((timebox[0].offsetHeight - parentHeight) * percentage, 10)); }, 10); }) .on('resize_scroll.xdsoft_scroller', function (event, percentage, noTriggerScroll) { var percent, sh; parentHeight = timeboxparent[0].clientHeight; height = timebox[0].offsetHeight; percent = parentHeight / height; sh = percent * scrollbar[0].offsetHeight; if (percent > 1) { scroller.hide(); } else { scroller.show(); scroller.css('height', parseInt(sh > 10 ? sh : 10, 10)); maximumOffset = scrollbar[0].offsetHeight - scroller[0].offsetHeight; if (noTriggerScroll !== true) { timeboxparent.trigger('scroll_element.xdsoft_scroller', [percentage || Math.abs(parseInt(timebox.css('marginTop'), 10)) / (height - parentHeight)]); } } }); timeboxparent.on('mousewheel', function (event) { var top = Math.abs(parseInt(timebox.css('marginTop'), 10)); top = top - (event.deltaY * 20); if (top < 0) { top = 0; } timeboxparent.trigger('scroll_element.xdsoft_scroller', [top / (height - parentHeight)]); event.stopPropagation(); return false; }); timeboxparent.on('touchstart', function (event) { start = pointerEventToXY(event); startTop = Math.abs(parseInt(timebox.css('marginTop'), 10)); }); timeboxparent.on('touchmove', function (event) { if (start) { event.preventDefault(); var coord = pointerEventToXY(event); timeboxparent.trigger('scroll_element.xdsoft_scroller', [(startTop - (coord.y - start.y)) / (height - parentHeight)]); } }); timeboxparent.on('touchend touchcancel', function (event) { start = false; startTop = 0; }); } timeboxparent.trigger('resize_scroll.xdsoft_scroller', [percent]); }); }; $.fn.datetimepicker = function (opt) { var KEY0 = 48, KEY9 = 57, _KEY0 = 96, _KEY9 = 105, CTRLKEY = 17, DEL = 46, ENTER = 13, ESC = 27, BACKSPACE = 8, ARROWLEFT = 37, ARROWUP = 38, ARROWRIGHT = 39, ARROWDOWN = 40, TAB = 9, F5 = 116, AKEY = 65, CKEY = 67, VKEY = 86, ZKEY = 90, YKEY = 89, ctrlDown = false, options = ($.isPlainObject(opt) || !opt) ? $.extend(true, {}, default_options, opt) : $.extend(true, {}, default_options), lazyInitTimer = 0, createDateTimePicker, destroyDateTimePicker, lazyInit = function (input) { input .on('open.xdsoft focusin.xdsoft mousedown.xdsoft', function initOnActionCallback(event) { if (input.is(':disabled') || input.data('xdsoft_datetimepicker')) { return; } clearTimeout(lazyInitTimer); lazyInitTimer = setTimeout(function () { if (!input.data('xdsoft_datetimepicker')) { createDateTimePicker(input); } input .off('open.xdsoft focusin.xdsoft mousedown.xdsoft', initOnActionCallback) .trigger('open.xdsoft'); }, 100); }); }; createDateTimePicker = function (input) { var datetimepicker = $('
'), xdsoft_copyright = $(''), datepicker = $('
'), mounth_picker = $('
' + '
' + '
' + '
'), calendar = $('
'), timepicker = $('
'), timeboxparent = timepicker.find('.xdsoft_time_box').eq(0), timebox = $('
'), applyButton = $(''), /*scrollbar = $('
'), scroller = $('
'),*/ monthselect = $('
'), yearselect = $('
'), triggerAfterOpen = false, XDSoft_datetime, //scroll_element, xchangeTimer, timerclick, current_time_index, setPos, timer = 0, timer1 = 0, _xdsoft_datetime; if (options.id) datetimepicker.attr('id', options.id); if (options.style) datetimepicker.attr('style', options.style); if (options.weeks) datetimepicker.addClass('xdsoft_showweeks'); datetimepicker.addClass('xdsoft_' + options.theme); datetimepicker.addClass(options.className); mounth_picker .find('.xdsoft_month span') .after(monthselect); mounth_picker .find('.xdsoft_year span') .after(yearselect); mounth_picker .find('.xdsoft_month,.xdsoft_year') .on('mousedown.xdsoft', function (event) { var select = $(this).find('.xdsoft_select').eq(0), val = 0, top = 0, visible = select.is(':visible'), items, i; mounth_picker .find('.xdsoft_select') .hide(); if (_xdsoft_datetime.currentTime) { val = _xdsoft_datetime.currentTime[$(this).hasClass('xdsoft_month') ? 'getMonth' : 'getFullYear'](); } select[visible ? 'hide' : 'show'](); for (items = select.find('div.xdsoft_option'), i = 0; i < items.length; i += 1) { if (items.eq(i).data('value') === val) { break; } else { top += items[0].offsetHeight; } } select.xdsoftScroller(top / (select.children()[0].offsetHeight - (select[0].clientHeight))); event.stopPropagation(); return false; }); mounth_picker .find('.xdsoft_select') .xdsoftScroller() .on('mousedown.xdsoft', function (event) { event.stopPropagation(); event.preventDefault(); }) .on('mousedown.xdsoft', '.xdsoft_option', function (event) { if (_xdsoft_datetime.currentTime === undefined || _xdsoft_datetime.currentTime === null) { _xdsoft_datetime.currentTime = _xdsoft_datetime.now(); } var year = _xdsoft_datetime.currentTime.getFullYear(); if (_xdsoft_datetime && _xdsoft_datetime.currentTime) { _xdsoft_datetime.currentTime[$(this).parent().parent().hasClass('xdsoft_monthselect') ? 'setMonth' : 'setFullYear']($(this).data('value')); } $(this).parent().parent().hide(); datetimepicker.trigger('xchange.xdsoft'); if (options.onChangeMonth && $.isFunction(options.onChangeMonth)) { options.onChangeMonth.call(datetimepicker, _xdsoft_datetime.currentTime, datetimepicker.data('input')); } if (year !== _xdsoft_datetime.currentTime.getFullYear() && $.isFunction(options.onChangeYear)) { options.onChangeYear.call(datetimepicker, _xdsoft_datetime.currentTime, datetimepicker.data('input')); } }); datetimepicker.setOptions = function (_options) { var highlightedDates = {}, getCaretPos = function (input) { try { if (document.selection && document.selection.createRange) { var range = document.selection.createRange(); return range.getBookmark().charCodeAt(2) - 2; } if (input.setSelectionRange) { return input.selectionStart; } } catch (e) { return 0; } }, setCaretPos = function (node, pos) { node = (typeof node === "string" || node instanceof String) ? document.getElementById(node) : node; if (!node) { return false; } if (node.createTextRange) { var textRange = node.createTextRange(); textRange.collapse(true); textRange.moveEnd('character', pos); textRange.moveStart('character', pos); textRange.select(); return true; } if (node.setSelectionRange) { node.setSelectionRange(pos, pos); return true; } return false; }, isValidValue = function (mask, value) { var reg = mask .replace(/([\[\]\/\{\}\(\)\-\.\+]{1})/g, '\\$1') .replace(/_/g, '{digit+}') .replace(/([0-9]{1})/g, '{digit$1}') .replace(/\{digit([0-9]{1})\}/g, '[0-$1_]{1}') .replace(/\{digit[\+]\}/g, '[0-9_]{1}'); return (new RegExp(reg)).test(value); }; options = $.extend(true, {}, options, _options); if (_options.allowTimes && $.isArray(_options.allowTimes) && _options.allowTimes.length) { options.allowTimes = $.extend(true, [], _options.allowTimes); } if (_options.weekends && $.isArray(_options.weekends) && _options.weekends.length) { options.weekends = $.extend(true, [], _options.weekends); } if (_options.highlightedDates && $.isArray(_options.highlightedDates) && _options.highlightedDates.length) { $.each(_options.highlightedDates, function (index, value) { var splitData = $.map(value.split(','), $.trim), exDesc, hDate = new HighlightedDate(Date.parseDate(splitData[0], options.formatDate), splitData[1], splitData[2]), // date, desc, style keyDate = hDate.date.dateFormat(options.formatDate); if (highlightedDates[keyDate] !== undefined) { exDesc = highlightedDates[keyDate].desc; if (exDesc && exDesc.length && hDate.desc && hDate.desc.length) { highlightedDates[keyDate].desc = exDesc + "\n" + hDate.desc; } } else { highlightedDates[keyDate] = hDate; } }); options.highlightedDates = $.extend(true, [], highlightedDates); } if (_options.highlightedPeriods && $.isArray(_options.highlightedPeriods) && _options.highlightedPeriods.length) { highlightedDates = $.extend(true, [], options.highlightedDates); $.each(_options.highlightedPeriods, function (index, value) { var splitData = $.map(value.split(','), $.trim), dateTest = Date.parseDate(splitData[0], options.formatDate), // start date dateEnd = Date.parseDate(splitData[1], options.formatDate), desc = splitData[2], hDate, keyDate, exDesc, style = splitData[3]; while (dateTest <= dateEnd) { hDate = new HighlightedDate(dateTest, desc, style); keyDate = dateTest.dateFormat(options.formatDate); dateTest.setDate(dateTest.getDate() + 1); if (highlightedDates[keyDate] !== undefined) { exDesc = highlightedDates[keyDate].desc; if (exDesc && exDesc.length && hDate.desc && hDate.desc.length) { highlightedDates[keyDate].desc = exDesc + "\n" + hDate.desc; } } else { highlightedDates[keyDate] = hDate; } } }); options.highlightedDates = $.extend(true, [], highlightedDates); } if (_options.disabledDates && $.isArray(_options.disabledDates) && _options.disabledDates.length) { options.disabledDates = $.extend(true, [], _options.disabledDates); } if ((options.open || options.opened) && (!options.inline)) { input.trigger('open.xdsoft'); } if (options.inline) { triggerAfterOpen = true; datetimepicker.addClass('xdsoft_inline'); input.after(datetimepicker).hide(); } if (options.inverseButton) { options.next = 'xdsoft_prev'; options.prev = 'xdsoft_next'; } if (options.datepicker) { datepicker.addClass('active'); } else { datepicker.removeClass('active'); } if (options.timepicker) { timepicker.addClass('active'); } else { timepicker.removeClass('active'); } if (options.value) { _xdsoft_datetime.setCurrentTime(options.value); if (input && input.val) { input.val(_xdsoft_datetime.str); } } if (isNaN(options.dayOfWeekStart)) { options.dayOfWeekStart = 0; } else { options.dayOfWeekStart = parseInt(options.dayOfWeekStart, 10) % 7; } if (!options.timepickerScrollbar) { timeboxparent.xdsoftScroller('hide'); } if (options.minDate && /^-(.*)$/.test(options.minDate)) { options.minDate = _xdsoft_datetime.strToDateTime(options.minDate).dateFormat(options.formatDate); } if (options.maxDate && /^\+(.*)$/.test(options.maxDate)) { options.maxDate = _xdsoft_datetime.strToDateTime(options.maxDate).dateFormat(options.formatDate); } applyButton.toggle(options.showApplyButton); mounth_picker .find('.xdsoft_today_button') .css('visibility', !options.todayButton ? 'hidden' : 'visible'); mounth_picker .find('.' + options.prev) .css('visibility', !options.prevButton ? 'hidden' : 'visible'); mounth_picker .find('.' + options.next) .css('visibility', !options.nextButton ? 'hidden' : 'visible'); if (options.mask) { input.off('keydown.xdsoft'); if (options.mask === true) { options.mask = options.format .replace(/Y/g, '9999') .replace(/F/g, '9999') .replace(/m/g, '19') .replace(/d/g, '39') .replace(/H/g, '29') .replace(/i/g, '59') .replace(/s/g, '59'); } if ($.type(options.mask) === 'string') { if (!isValidValue(options.mask, input.val())) { input.val(options.mask.replace(/[0-9]/g, '_')); } input.on('keydown.xdsoft', function (event) { var val = this.value, key = event.which, pos, digit; if (((key >= KEY0 && key <= KEY9) || (key >= _KEY0 && key <= _KEY9)) || (key === BACKSPACE || key === DEL)) { pos = getCaretPos(this); digit = (key !== BACKSPACE && key !== DEL) ? String.fromCharCode((_KEY0 <= key && key <= _KEY9) ? key - KEY0 : key) : '_'; if ((key === BACKSPACE || key === DEL) && pos) { pos -= 1; digit = '_'; } while (/[^0-9_]/.test(options.mask.substr(pos, 1)) && pos < options.mask.length && pos > 0) { pos += (key === BACKSPACE || key === DEL) ? -1 : 1; } val = val.substr(0, pos) + digit + val.substr(pos + 1); if ($.trim(val) === '') { val = options.mask.replace(/[0-9]/g, '_'); } else { if (pos === options.mask.length) { event.preventDefault(); return false; } } pos += (key === BACKSPACE || key === DEL) ? 0 : 1; while (/[^0-9_]/.test(options.mask.substr(pos, 1)) && pos < options.mask.length && pos > 0) { pos += (key === BACKSPACE || key === DEL) ? -1 : 1; } if (isValidValue(options.mask, val)) { this.value = val; setCaretPos(this, pos); } else if ($.trim(val) === '') { this.value = options.mask.replace(/[0-9]/g, '_'); } else { input.trigger('error_input.xdsoft'); } } else { if (([AKEY, CKEY, VKEY, ZKEY, YKEY].indexOf(key) !== -1 && ctrlDown) || [ESC, ARROWUP, ARROWDOWN, ARROWLEFT, ARROWRIGHT, F5, CTRLKEY, TAB, ENTER].indexOf(key) !== -1) { return true; } } event.preventDefault(); return false; }); } } if (options.validateOnBlur) { input .off('blur.xdsoft') .on('blur.xdsoft', function () { if (options.allowBlank && !$.trim($(this).val()).length) { $(this).val(null); datetimepicker.data('xdsoft_datetime').empty(); } else if (!Date.parseDate($(this).val(), options.format)) { var splittedHours = +([$(this).val()[0], $(this).val()[1]].join('')), splittedMinutes = +([$(this).val()[2], $(this).val()[3]].join('')); // parse the numbers as 0312 => 03:12 if (!options.datepicker && options.timepicker && splittedHours >= 0 && splittedHours < 24 && splittedMinutes >= 0 && splittedMinutes < 60) { $(this).val([splittedHours, splittedMinutes].map(function (item) { return item > 9 ? item : '0' + item; }).join(':')); } else { $(this).val((_xdsoft_datetime.now()).dateFormat(options.format)); } datetimepicker.data('xdsoft_datetime').setCurrentTime($(this).val()); } else { datetimepicker.data('xdsoft_datetime').setCurrentTime($(this).val()); } datetimepicker.trigger('changedatetime.xdsoft'); }); } options.dayOfWeekStartPrev = (options.dayOfWeekStart === 0) ? 6 : options.dayOfWeekStart - 1; datetimepicker .trigger('xchange.xdsoft') .trigger('afterOpen.xdsoft'); }; datetimepicker .data('options', options) .on('mousedown.xdsoft', function (event) { event.stopPropagation(); event.preventDefault(); yearselect.hide(); monthselect.hide(); return false; }); //scroll_element = timepicker.find('.xdsoft_time_box'); timeboxparent.append(timebox); timeboxparent.xdsoftScroller(); datetimepicker.on('afterOpen.xdsoft', function () { timeboxparent.xdsoftScroller(); }); datetimepicker .append(datepicker) .append(timepicker); if (options.withoutCopyright !== true) { datetimepicker .append(xdsoft_copyright); } datepicker .append(mounth_picker) .append(calendar) .append(applyButton); $(options.parentID) .append(datetimepicker); XDSoft_datetime = function () { var _this = this; _this.now = function (norecursion) { var d = new Date(), date, time; if (!norecursion && options.defaultDate) { date = _this.strToDateTime(options.defaultDate); d.setFullYear(date.getFullYear()); d.setMonth(date.getMonth()); d.setDate(date.getDate()); } if (options.yearOffset) { d.setFullYear(d.getFullYear() + options.yearOffset); } if (!norecursion && options.defaultTime) { time = _this.strtotime(options.defaultTime); d.setHours(time.getHours()); d.setMinutes(time.getMinutes()); } return d; }; _this.isValidDate = function (d) { if (Object.prototype.toString.call(d) !== "[object Date]") { return false; } return !isNaN(d.getTime()); }; _this.setCurrentTime = function (dTime) { _this.currentTime = (typeof dTime === 'string') ? _this.strToDateTime(dTime) : _this.isValidDate(dTime) ? dTime : _this.now(); datetimepicker.trigger('xchange.xdsoft'); }; _this.empty = function () { _this.currentTime = null; }; _this.getCurrentTime = function (dTime) { return _this.currentTime; }; _this.nextMonth = function () { if (_this.currentTime === undefined || _this.currentTime === null) { _this.currentTime = _this.now(); } var month = _this.currentTime.getMonth() + 1, year; if (month === 12) { _this.currentTime.setFullYear(_this.currentTime.getFullYear() + 1); month = 0; } year = _this.currentTime.getFullYear(); _this.currentTime.setDate( Math.min( new Date(_this.currentTime.getFullYear(), month + 1, 0).getDate(), _this.currentTime.getDate() ) ); _this.currentTime.setMonth(month); if (options.onChangeMonth && $.isFunction(options.onChangeMonth)) { options.onChangeMonth.call(datetimepicker, _xdsoft_datetime.currentTime, datetimepicker.data('input')); } if (year !== _this.currentTime.getFullYear() && $.isFunction(options.onChangeYear)) { options.onChangeYear.call(datetimepicker, _xdsoft_datetime.currentTime, datetimepicker.data('input')); } datetimepicker.trigger('xchange.xdsoft'); return month; }; _this.prevMonth = function () { if (_this.currentTime === undefined || _this.currentTime === null) { _this.currentTime = _this.now(); } var month = _this.currentTime.getMonth() - 1; if (month === -1) { _this.currentTime.setFullYear(_this.currentTime.getFullYear() - 1); month = 11; } _this.currentTime.setDate( Math.min( new Date(_this.currentTime.getFullYear(), month + 1, 0).getDate(), _this.currentTime.getDate() ) ); _this.currentTime.setMonth(month); if (options.onChangeMonth && $.isFunction(options.onChangeMonth)) { options.onChangeMonth.call(datetimepicker, _xdsoft_datetime.currentTime, datetimepicker.data('input')); } datetimepicker.trigger('xchange.xdsoft'); return month; }; _this.getWeekOfYear = function (datetime) { var onejan = new Date(datetime.getFullYear(), 0, 1); return Math.ceil((((datetime - onejan) / 86400000) + onejan.getDay() + 1) / 7); }; _this.strToDateTime = function (sDateTime) { var tmpDate = [], timeOffset, currentTime; if (sDateTime && sDateTime instanceof Date && _this.isValidDate(sDateTime)) { return sDateTime; } tmpDate = /^(\+|\-)(.*)$/.exec(sDateTime); if (tmpDate) { tmpDate[2] = Date.parseDate(tmpDate[2], options.formatDate); } if (tmpDate && tmpDate[2]) { timeOffset = tmpDate[2].getTime() - (tmpDate[2].getTimezoneOffset()) * 60000; currentTime = new Date((_this.now(true)).getTime() + parseInt(tmpDate[1] + '1', 10) * timeOffset); } else { currentTime = sDateTime ? Date.parseDate(sDateTime, options.format) : _this.now(); } if (!_this.isValidDate(currentTime)) { currentTime = _this.now(); } return currentTime; }; _this.strToDate = function (sDate) { if (sDate && sDate instanceof Date && _this.isValidDate(sDate)) { return sDate; } var currentTime = sDate ? Date.parseDate(sDate, options.formatDate) : _this.now(true); if (!_this.isValidDate(currentTime)) { currentTime = _this.now(true); } return currentTime; }; _this.strtotime = function (sTime) { if (sTime && sTime instanceof Date && _this.isValidDate(sTime)) { return sTime; } var currentTime = sTime ? Date.parseDate(sTime, options.formatTime) : _this.now(true); if (!_this.isValidDate(currentTime)) { currentTime = _this.now(true); } return currentTime; }; _this.str = function () { return _this.currentTime.dateFormat(options.format); }; _this.currentTime = this.now(); }; _xdsoft_datetime = new XDSoft_datetime(); applyButton.on('click', function (e) {//pathbrite e.preventDefault(); datetimepicker.data('changed', true); _xdsoft_datetime.setCurrentTime(getCurrentValue()); input.val(_xdsoft_datetime.str()); datetimepicker.trigger('close.xdsoft'); }); mounth_picker .find('.xdsoft_today_button') .on('mousedown.xdsoft', function () { datetimepicker.data('changed', true); _xdsoft_datetime.setCurrentTime(0); datetimepicker.trigger('afterOpen.xdsoft'); }).on('dblclick.xdsoft', function () { var currentDate = _xdsoft_datetime.getCurrentTime(); currentDate = new Date(currentDate.getFullYear(),currentDate.getMonth(),currentDate.getDate()); var minDate = _xdsoft_datetime.strToDate(options.minDate); minDate = new Date(minDate.getFullYear(),minDate.getMonth(),minDate.getDate()); if(currentDate < minDate) { return; } var maxDate = _xdsoft_datetime.strToDate(options.maxDate); maxDate = new Date(maxDate.getFullYear(),maxDate.getMonth(),maxDate.getDate()); if(currentDate > maxDate) { return; } input.val(_xdsoft_datetime.str()); datetimepicker.trigger('close.xdsoft'); }); mounth_picker .find('.xdsoft_prev,.xdsoft_next') .on('mousedown.xdsoft', function () { var $this = $(this), timer = 0, stop = false; (function arguments_callee1(v) { if ($this.hasClass(options.next)) { _xdsoft_datetime.nextMonth(); } else if ($this.hasClass(options.prev)) { _xdsoft_datetime.prevMonth(); } if (options.monthChangeSpinner) { if (!stop) { timer = setTimeout(arguments_callee1, v || 100); } } }(500)); $([document.body, window]).on('mouseup.xdsoft', function arguments_callee2() { clearTimeout(timer); stop = true; $([document.body, window]).off('mouseup.xdsoft', arguments_callee2); }); }); timepicker .find('.xdsoft_prev,.xdsoft_next') .on('mousedown.xdsoft', function () { var $this = $(this), timer = 0, stop = false, period = 110; (function arguments_callee4(v) { var pheight = timeboxparent[0].clientHeight, height = timebox[0].offsetHeight, top = Math.abs(parseInt(timebox.css('marginTop'), 10)); if ($this.hasClass(options.next) && (height - pheight) - options.timeHeightInTimePicker >= top) { timebox.css('marginTop', '-' + (top + options.timeHeightInTimePicker) + 'px'); } else if ($this.hasClass(options.prev) && top - options.timeHeightInTimePicker >= 0) { timebox.css('marginTop', '-' + (top - options.timeHeightInTimePicker) + 'px'); } timeboxparent.trigger('scroll_element.xdsoft_scroller', [Math.abs(parseInt(timebox.css('marginTop'), 10) / (height - pheight))]); period = (period > 10) ? 10 : period - 10; if (!stop) { timer = setTimeout(arguments_callee4, v || period); } }(500)); $([document.body, window]).on('mouseup.xdsoft', function arguments_callee5() { clearTimeout(timer); stop = true; $([document.body, window]) .off('mouseup.xdsoft', arguments_callee5); }); }); xchangeTimer = 0; // base handler - generating a calendar and timepicker datetimepicker .on('xchange.xdsoft', function (event) { clearTimeout(xchangeTimer); xchangeTimer = setTimeout(function () { if (_xdsoft_datetime.currentTime === undefined || _xdsoft_datetime.currentTime === null) { _xdsoft_datetime.currentTime = _xdsoft_datetime.now(); } var table = '', start = new Date(_xdsoft_datetime.currentTime.getFullYear(), _xdsoft_datetime.currentTime.getMonth(), 1, 12, 0, 0), i = 0, j, today = _xdsoft_datetime.now(), maxDate = false, minDate = false, hDate, d, y, m, w, classes = [], customDateSettings, newRow = true, time = '', h = '', line_time, description; while (start.getDay() !== options.dayOfWeekStart) { start.setDate(start.getDate() - 1); } table += ''; if (options.weeks) { table += ''; } for (j = 0; j < 7; j += 1) { table += ''; } table += ''; table += ''; if (options.maxDate !== false) { maxDate = _xdsoft_datetime.strToDate(options.maxDate); maxDate = new Date(maxDate.getFullYear(), maxDate.getMonth(), maxDate.getDate(), 23, 59, 59, 999); } if (options.minDate !== false) { minDate = _xdsoft_datetime.strToDate(options.minDate); minDate = new Date(minDate.getFullYear(), minDate.getMonth(), minDate.getDate()); } while (i < _xdsoft_datetime.currentTime.countDaysInMonth() || start.getDay() !== options.dayOfWeekStart || _xdsoft_datetime.currentTime.getMonth() === start.getMonth()) { classes = []; i += 1; d = start.getDate(); y = start.getFullYear(); m = start.getMonth(); w = _xdsoft_datetime.getWeekOfYear(start); description = ''; classes.push('xdsoft_date'); if (options.beforeShowDay && $.isFunction(options.beforeShowDay.call)) { customDateSettings = options.beforeShowDay.call(datetimepicker, start); } else { customDateSettings = null; } if ((maxDate !== false && start > maxDate) || (minDate !== false && start < minDate) || (customDateSettings && customDateSettings[0] === false)) { classes.push('xdsoft_disabled'); } else if (options.disabledDates.indexOf(start.dateFormat(options.formatDate)) !== -1) { classes.push('xdsoft_disabled'); } if (customDateSettings && customDateSettings[1] !== "") { classes.push(customDateSettings[1]); } if (_xdsoft_datetime.currentTime.getMonth() !== m) { classes.push('xdsoft_other_month'); } if ((options.defaultSelect || datetimepicker.data('changed')) && _xdsoft_datetime.currentTime.dateFormat(options.formatDate) === start.dateFormat(options.formatDate)) { classes.push('xdsoft_current'); } if (today.dateFormat(options.formatDate) === start.dateFormat(options.formatDate)) { classes.push('xdsoft_today'); } if (start.getDay() === 0 || start.getDay() === 6 || options.weekends.indexOf(start.dateFormat(options.formatDate)) !== -1) { classes.push('xdsoft_weekend'); } if (options.highlightedDates[start.dateFormat(options.formatDate)] !== undefined) { hDate = options.highlightedDates[start.dateFormat(options.formatDate)]; classes.push(hDate.style === undefined ? 'xdsoft_highlighted_default' : hDate.style); description = hDate.desc === undefined ? '' : hDate.desc; } if (options.beforeShowDay && $.isFunction(options.beforeShowDay)) { classes.push(options.beforeShowDay(start)); } if (newRow) { table += ''; newRow = false; if (options.weeks) { table += ''; } } table += ''; if (start.getDay() === options.dayOfWeekStartPrev) { table += ''; newRow = true; } start.setDate(d + 1); } table += '
' + options.i18n[options.lang].dayOfWeek[(j + options.dayOfWeekStart) % 7] + '
' + w + '' + '
' + d + '
' + '
'; calendar.html(table); mounth_picker.find('.xdsoft_label span').eq(0).text(options.i18n[options.lang].months[_xdsoft_datetime.currentTime.getMonth()]); mounth_picker.find('.xdsoft_label span').eq(1).text(_xdsoft_datetime.currentTime.getFullYear()); // generate timebox time = ''; h = ''; m = ''; line_time = function line_time(h, m) { var now = _xdsoft_datetime.now(), optionDateTime, current_time; now.setHours(h); h = parseInt(now.getHours(), 10); now.setMinutes(m); m = parseInt(now.getMinutes(), 10); optionDateTime = new Date(_xdsoft_datetime.currentTime); optionDateTime.setHours(h); optionDateTime.setMinutes(m); classes = []; if ((options.minDateTime !== false && options.minDateTime > optionDateTime) || (options.maxTime !== false && _xdsoft_datetime.strtotime(options.maxTime).getTime() < now.getTime()) || (options.minTime !== false && _xdsoft_datetime.strtotime(options.minTime).getTime() > now.getTime())) { classes.push('xdsoft_disabled'); } current_time = new Date(_xdsoft_datetime.currentTime); current_time.setHours(parseInt(_xdsoft_datetime.currentTime.getHours(), 10)); current_time.setMinutes(Math[options.roundTime](_xdsoft_datetime.currentTime.getMinutes() / options.step) * options.step); if ((options.initTime || options.defaultSelect || datetimepicker.data('changed')) && current_time.getHours() === parseInt(h, 10) && (options.step > 59 || current_time.getMinutes() === parseInt(m, 10))) { if (options.defaultSelect || datetimepicker.data('changed')) { classes.push('xdsoft_current'); } else if (options.initTime) { classes.push('xdsoft_init_time'); } } if (parseInt(today.getHours(), 10) === parseInt(h, 10) && parseInt(today.getMinutes(), 10) === parseInt(m, 10)) { classes.push('xdsoft_today'); } time += '
' + now.dateFormat(options.formatTime) + '
'; }; if (!options.allowTimes || !$.isArray(options.allowTimes) || !options.allowTimes.length) { for (i = 0, j = 0; i < (options.hours12 ? 12 : 24); i += 1) { for (j = 0; j < 60; j += options.step) { h = (i < 10 ? '0' : '') + i; m = (j < 10 ? '0' : '') + j; line_time(h, m); } } } else { for (i = 0; i < options.allowTimes.length; i += 1) { h = _xdsoft_datetime.strtotime(options.allowTimes[i]).getHours(); m = _xdsoft_datetime.strtotime(options.allowTimes[i]).getMinutes(); line_time(h, m); } } timebox.html(time); opt = ''; i = 0; for (i = parseInt(options.yearStart, 10) + options.yearOffset; i <= parseInt(options.yearEnd, 10) + options.yearOffset; i += 1) { opt += '
' + i + '
'; } yearselect.children().eq(0) .html(opt); for (i = parseInt(options.monthStart, 10), opt = ''; i <= parseInt(options.monthEnd, 10); i += 1) { opt += '
' + options.i18n[options.lang].months[i] + '
'; } monthselect.children().eq(0).html(opt); $(datetimepicker) .trigger('generate.xdsoft'); }, 10); event.stopPropagation(); }) .on('afterOpen.xdsoft', function () { if (options.timepicker) { var classType, pheight, height, top; if (timebox.find('.xdsoft_current').length) { classType = '.xdsoft_current'; } else if (timebox.find('.xdsoft_init_time').length) { classType = '.xdsoft_init_time'; } if (classType) { pheight = timeboxparent[0].clientHeight; height = timebox[0].offsetHeight; top = timebox.find(classType).index() * options.timeHeightInTimePicker + 1; if ((height - pheight) < top) { top = height - pheight; } timeboxparent.trigger('scroll_element.xdsoft_scroller', [parseInt(top, 10) / (height - pheight)]); } else { timeboxparent.trigger('scroll_element.xdsoft_scroller', [0]); } } }); timerclick = 0; calendar .on('click.xdsoft', 'td', function (xdevent) { xdevent.stopPropagation(); // Prevents closing of Pop-ups, Modals and Flyouts in Bootstrap timerclick += 1; var $this = $(this), currentTime = _xdsoft_datetime.currentTime; if (currentTime === undefined || currentTime === null) { _xdsoft_datetime.currentTime = _xdsoft_datetime.now(); currentTime = _xdsoft_datetime.currentTime; } if ($this.hasClass('xdsoft_disabled')) { return false; } currentTime.setDate(1); currentTime.setFullYear($this.data('year')); currentTime.setMonth($this.data('month')); currentTime.setDate($this.data('date')); datetimepicker.trigger('select.xdsoft', [currentTime]); input.val(_xdsoft_datetime.str()); if ((timerclick > 1 || (options.closeOnDateSelect === true || (options.closeOnDateSelect === 0 && !options.timepicker))) && !options.inline) { datetimepicker.trigger('close.xdsoft'); } if (options.onSelectDate && $.isFunction(options.onSelectDate)) { options.onSelectDate.call(datetimepicker, _xdsoft_datetime.currentTime, datetimepicker.data('input'), xdevent); } datetimepicker.data('changed', true); datetimepicker.trigger('xchange.xdsoft'); datetimepicker.trigger('changedatetime.xdsoft'); setTimeout(function () { timerclick = 0; }, 200); }); timebox .on('click.xdsoft', 'div', function (xdevent) { xdevent.stopPropagation(); var $this = $(this), currentTime = _xdsoft_datetime.currentTime; if (currentTime === undefined || currentTime === null) { _xdsoft_datetime.currentTime = _xdsoft_datetime.now(); currentTime = _xdsoft_datetime.currentTime; } if ($this.hasClass('xdsoft_disabled')) { return false; } currentTime.setHours($this.data('hour')); currentTime.setMinutes($this.data('minute')); datetimepicker.trigger('select.xdsoft', [currentTime]); datetimepicker.data('input').val(_xdsoft_datetime.str()); if (options.inline !== true && options.closeOnTimeSelect === true) { datetimepicker.trigger('close.xdsoft'); } if (options.onSelectTime && $.isFunction(options.onSelectTime)) { options.onSelectTime.call(datetimepicker, _xdsoft_datetime.currentTime, datetimepicker.data('input'), xdevent); } datetimepicker.data('changed', true); datetimepicker.trigger('xchange.xdsoft'); datetimepicker.trigger('changedatetime.xdsoft'); }); datepicker .on('mousewheel.xdsoft', function (event) { if (!options.scrollMonth) { return true; } if (event.deltaY < 0) { _xdsoft_datetime.nextMonth(); } else { _xdsoft_datetime.prevMonth(); } return false; }); input .on('mousewheel.xdsoft', function (event) { if (!options.scrollInput) { return true; } if (!options.datepicker && options.timepicker) { current_time_index = timebox.find('.xdsoft_current').length ? timebox.find('.xdsoft_current').eq(0).index() : 0; if (current_time_index + event.deltaY >= 0 && current_time_index + event.deltaY < timebox.children().length) { current_time_index += event.deltaY; } if (timebox.children().eq(current_time_index).length) { timebox.children().eq(current_time_index).trigger('mousedown'); } return false; } if (options.datepicker && !options.timepicker) { datepicker.trigger(event, [event.deltaY, event.deltaX, event.deltaY]); if (input.val) { input.val(_xdsoft_datetime.str()); } datetimepicker.trigger('changedatetime.xdsoft'); return false; } }); datetimepicker .on('changedatetime.xdsoft', function (event) { if (options.onChangeDateTime && $.isFunction(options.onChangeDateTime)) { var $input = datetimepicker.data('input'); options.onChangeDateTime.call(datetimepicker, _xdsoft_datetime.currentTime, $input, event); delete options.value; $input.trigger('change'); } }) .on('generate.xdsoft', function () { if (options.onGenerate && $.isFunction(options.onGenerate)) { options.onGenerate.call(datetimepicker, _xdsoft_datetime.currentTime, datetimepicker.data('input')); } if (triggerAfterOpen) { datetimepicker.trigger('afterOpen.xdsoft'); triggerAfterOpen = false; } }) .on('click.xdsoft', function (xdevent) { xdevent.stopPropagation(); }); current_time_index = 0; setPos = function () { var offset = datetimepicker.data('input').offset(), top = offset.top + datetimepicker.data('input')[0].offsetHeight - 1, left = offset.left, position = "absolute"; if (options.fixed) { top -= $(window).scrollTop(); left -= $(window).scrollLeft(); position = "fixed"; } else { if (top + datetimepicker[0].offsetHeight > $(window).height() + $(window).scrollTop()) { top = offset.top - datetimepicker[0].offsetHeight + 1; } if (top < 0) { top = 0; } if (left + datetimepicker[0].offsetWidth > $(window).width()) { left = $(window).width() - datetimepicker[0].offsetWidth; } } var node = datetimepicker[0] do { node = node.parentNode; if(window.getComputedStyle(node).getPropertyValue('position') === 'relative' && $(window).width() >= node.offsetWidth) { left = left - (($(window).width() - node.offsetWidth)/2) break } } while(node.nodeName != 'HTML') datetimepicker.css({ left: left, top: top, position: position }); }; datetimepicker .on('open.xdsoft', function (event) { var onShow = true; if (options.onShow && $.isFunction(options.onShow)) { onShow = options.onShow.call(datetimepicker, _xdsoft_datetime.currentTime, datetimepicker.data('input'), event); } if (onShow !== false) { datetimepicker.show(); setPos(); $(window) .off('resize.xdsoft', setPos) .on('resize.xdsoft', setPos); if (options.closeOnWithoutClick) { $([document.body, window]).on('mousedown.xdsoft', function arguments_callee6() { datetimepicker.trigger('close.xdsoft'); $([document.body, window]).off('mousedown.xdsoft', arguments_callee6); }); } } }) .on('close.xdsoft', function (event) { var onClose = true; mounth_picker .find('.xdsoft_month,.xdsoft_year') .find('.xdsoft_select') .hide(); if (options.onClose && $.isFunction(options.onClose)) { onClose = options.onClose.call(datetimepicker, _xdsoft_datetime.currentTime, datetimepicker.data('input'), event); } if (onClose !== false && !options.opened && !options.inline) { datetimepicker.hide(); } event.stopPropagation(); }) .on('toggle.xdsoft', function (event) { if (datetimepicker.is(':visible')) { datetimepicker.trigger('close.xdsoft'); } else { datetimepicker.trigger('open.xdsoft'); } }) .data('input', input); timer = 0; timer1 = 0; datetimepicker.data('xdsoft_datetime', _xdsoft_datetime); datetimepicker.setOptions(options); function getCurrentValue() { var ct = false, time; if (options.startDate) { ct = _xdsoft_datetime.strToDate(options.startDate); } else { ct = options.value || ((input && input.val && input.val()) ? input.val() : ''); if (ct) { ct = _xdsoft_datetime.strToDateTime(ct); } else if (options.defaultDate) { ct = _xdsoft_datetime.strToDateTime(options.defaultDate); if (options.defaultTime) { time = _xdsoft_datetime.strtotime(options.defaultTime); ct.setHours(time.getHours()); ct.setMinutes(time.getMinutes()); } } } if (ct && _xdsoft_datetime.isValidDate(ct)) { datetimepicker.data('changed', true); } else { ct = ''; } return ct || 0; } _xdsoft_datetime.setCurrentTime(getCurrentValue()); input .data('xdsoft_datetimepicker', datetimepicker) .on('open.xdsoft focusin.xdsoft mousedown.xdsoft', function (event) { if (input.is(':disabled') || (input.data('xdsoft_datetimepicker').is(':visible') && options.closeOnInputClick)) { return; } clearTimeout(timer); timer = setTimeout(function () { if (input.is(':disabled')) { return; } triggerAfterOpen = true; _xdsoft_datetime.setCurrentTime(getCurrentValue()); datetimepicker.trigger('open.xdsoft'); }, 100); }) .on('keydown.xdsoft', function (event) { var val = this.value, elementSelector, key = event.which; if ([ENTER].indexOf(key) !== -1 && options.enterLikeTab) { elementSelector = $("input:visible,textarea:visible"); datetimepicker.trigger('close.xdsoft'); elementSelector.eq(elementSelector.index(this) + 1).focus(); return false; } if ([TAB].indexOf(key) !== -1) { datetimepicker.trigger('close.xdsoft'); return true; } }); }; destroyDateTimePicker = function (input) { var datetimepicker = input.data('xdsoft_datetimepicker'); if (datetimepicker) { datetimepicker.data('xdsoft_datetime', null); datetimepicker.remove(); input .data('xdsoft_datetimepicker', null) .off('.xdsoft'); $(window).off('resize.xdsoft'); $([window, document.body]).off('mousedown.xdsoft'); if (input.unmousewheel) { input.unmousewheel(); } } }; $(document) .off('keydown.xdsoftctrl keyup.xdsoftctrl') .on('keydown.xdsoftctrl', function (e) { if (e.keyCode === CTRLKEY) { ctrlDown = true; } }) .on('keyup.xdsoftctrl', function (e) { if (e.keyCode === CTRLKEY) { ctrlDown = false; } }); return this.each(function () { var datetimepicker = $(this).data('xdsoft_datetimepicker'), $input; if (datetimepicker) { if ($.type(opt) === 'string') { switch (opt) { case 'show': $(this).select().focus(); datetimepicker.trigger('open.xdsoft'); break; case 'hide': datetimepicker.trigger('close.xdsoft'); break; case 'toggle': datetimepicker.trigger('toggle.xdsoft'); break; case 'destroy': destroyDateTimePicker($(this)); break; case 'reset': this.value = this.defaultValue; if (!this.value || !datetimepicker.data('xdsoft_datetime').isValidDate(Date.parseDate(this.value, options.format))) { datetimepicker.data('changed', false); } datetimepicker.data('xdsoft_datetime').setCurrentTime(this.value); break; case 'validate': $input = datetimepicker.data('input'); $input.trigger('blur.xdsoft'); break; } } else { datetimepicker .setOptions(opt); } return 0; } if ($.type(opt) !== 'string') { if (!options.lazyInit || options.open || options.inline) { createDateTimePicker($(this)); } else { lazyInit($(this)); } } }); }; $.fn.datetimepicker.defaults = default_options; }(jQuery)); function HighlightedDate(date, desc, style) { "use strict"; this.date = date; this.desc = desc; this.style = style; } (function () { /*! Copyright (c) 2013 Brandon Aaron (http://brandon.aaron.sh) * Licensed under the MIT License (LICENSE.txt). * * Version: 3.1.12 * * Requires: jQuery 1.2.2+ */ !function(a){"function"==typeof define&&define.amd?define(["jquery"],a):"object"==typeof exports?module.exports=a:a(jQuery)}(function(a){function b(b){var g=b||window.event,h=i.call(arguments,1),j=0,l=0,m=0,n=0,o=0,p=0;if(b=a.event.fix(g),b.type="mousewheel","detail"in g&&(m=-1*g.detail),"wheelDelta"in g&&(m=g.wheelDelta),"wheelDeltaY"in g&&(m=g.wheelDeltaY),"wheelDeltaX"in g&&(l=-1*g.wheelDeltaX),"axis"in g&&g.axis===g.HORIZONTAL_AXIS&&(l=-1*m,m=0),j=0===m?l:m,"deltaY"in g&&(m=-1*g.deltaY,j=m),"deltaX"in g&&(l=g.deltaX,0===m&&(j=-1*l)),0!==m||0!==l){if(1===g.deltaMode){var q=a.data(this,"mousewheel-line-height");j*=q,m*=q,l*=q}else if(2===g.deltaMode){var r=a.data(this,"mousewheel-page-height");j*=r,m*=r,l*=r}if(n=Math.max(Math.abs(m),Math.abs(l)),(!f||f>n)&&(f=n,d(g,n)&&(f/=40)),d(g,n)&&(j/=40,l/=40,m/=40),j=Math[j>=1?"floor":"ceil"](j/f),l=Math[l>=1?"floor":"ceil"](l/f),m=Math[m>=1?"floor":"ceil"](m/f),k.settings.normalizeOffset&&this.getBoundingClientRect){var s=this.getBoundingClientRect();o=b.clientX-s.left,p=b.clientY-s.top}return b.deltaX=l,b.deltaY=m,b.deltaFactor=f,b.offsetX=o,b.offsetY=p,b.deltaMode=0,h.unshift(b,j,l,m),e&&clearTimeout(e),e=setTimeout(c,200),(a.event.dispatch||a.event.handle).apply(this,h)}}function c(){f=null}function d(a,b){return k.settings.adjustOldDeltas&&"mousewheel"===a.type&&b%120===0}var e,f,g=["wheel","mousewheel","DOMMouseScroll","MozMousePixelScroll"],h="onwheel"in document||document.documentMode>=9?["wheel"]:["mousewheel","DomMouseScroll","MozMousePixelScroll"],i=Array.prototype.slice;if(a.event.fixHooks)for(var j=g.length;j;)a.event.fixHooks[g[--j]]=a.event.mouseHooks;var k=a.event.special.mousewheel={version:"3.1.12",setup:function(){if(this.addEventListener)for(var c=h.length;c;)this.addEventListener(h[--c],b,!1);else this.onmousewheel=b;a.data(this,"mousewheel-line-height",k.getLineHeight(this)),a.data(this,"mousewheel-page-height",k.getPageHeight(this))},teardown:function(){if(this.removeEventListener)for(var c=h.length;c;)this.removeEventListener(h[--c],b,!1);else this.onmousewheel=null;a.removeData(this,"mousewheel-line-height"),a.removeData(this,"mousewheel-page-height")},getLineHeight:function(b){var c=a(b),d=c["offsetParent"in a.fn?"offsetParent":"parent"]();return d.length||(d=a("body")),parseInt(d.css("fontSize"),10)||parseInt(c.css("fontSize"),10)||16},getPageHeight:function(b){return a(b).height()},settings:{adjustOldDeltas:!0,normalizeOffset:!0}};a.fn.extend({mousewheel:function(a){return a?this.bind("mousewheel",a):this.trigger("mousewheel")},unmousewheel:function(a){return this.unbind("mousewheel",a)}})}); // Parse and Format Library //http://www.xaprb.com/blog/2005/12/12/javascript-closures-for-runtime-efficiency/ /* * Copyright (C) 2004 Baron Schwartz * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as published by the * Free Software Foundation, version 2.1. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more * details. */ Date.parseFunctions={count:0};Date.parseRegexes=[];Date.formatFunctions={count:0};Date.prototype.dateFormat=function(b){if(b=="unixtime"){return parseInt(this.getTime()/1000);}if(Date.formatFunctions[b]==null){Date.createNewFormat(b);}var a=Date.formatFunctions[b];return this[a]();};Date.createNewFormat=function(format){var funcName="format"+Date.formatFunctions.count++;Date.formatFunctions[format]=funcName;var codePrefix="Date.prototype."+funcName+" = function() {return ";var code="";var special=false;var ch="";for(var i=0;i 0) {";var regex="";var special=false;var ch="";for(var i=0;i 0 && z > 0){\nvar doyDate = new Date(y,0);\ndoyDate.setDate(z);\nm = doyDate.getMonth();\nd = doyDate.getDate();\n}";code+="if (y > 0 && m >= 0 && d > 0 && h >= 0 && i >= 0 && s >= 0)\n{return new Date(y, m, d, h, i, s);}\nelse if (y > 0 && m >= 0 && d > 0 && h >= 0 && i >= 0)\n{return new Date(y, m, d, h, i);}\nelse if (y > 0 && m >= 0 && d > 0 && h >= 0)\n{return new Date(y, m, d, h);}\nelse if (y > 0 && m >= 0 && d > 0)\n{return new Date(y, m, d);}\nelse if (y > 0 && m >= 0)\n{return new Date(y, m);}\nelse if (y > 0)\n{return new Date(y);}\n}return null;}";Date.parseRegexes[regexNum]=new RegExp("^"+regex+"$",'i');eval(code);};Date.formatCodeToRegex=function(b,a){switch(b){case"D":return{g:0,c:null,s:"(?:Sun|Mon|Tue|Wed|Thu|Fri|Sat)"};case"j":case"d":return{g:1,c:"d = parseInt(results["+a+"], 10);\n",s:"(\\d{1,2})"};case"l":return{g:0,c:null,s:"(?:"+Date.dayNames.join("|")+")"};case"S":return{g:0,c:null,s:"(?:st|nd|rd|th)"};case"w":return{g:0,c:null,s:"\\d"};case"z":return{g:1,c:"z = parseInt(results["+a+"], 10);\n",s:"(\\d{1,3})"};case"W":return{g:0,c:null,s:"(?:\\d{2})"};case"F":return{g:1,c:"m = parseInt(Date.monthNumbers[results["+a+"].substring(0, 3)], 10);\n",s:"("+Date.monthNames.join("|")+")"};case"M":return{g:1,c:"m = parseInt(Date.monthNumbers[results["+a+"]], 10);\n",s:"(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)"};case"n":case"m":return{g:1,c:"m = parseInt(results["+a+"], 10) - 1;\n",s:"(\\d{1,2})"};case"t":return{g:0,c:null,s:"\\d{1,2}"};case"L":return{g:0,c:null,s:"(?:1|0)"};case"Y":return{g:1,c:"y = parseInt(results["+a+"], 10);\n",s:"(\\d{4})"};case"y":return{g:1,c:"var ty = parseInt(results["+a+"], 10);\ny = ty > Date.y2kYear ? 1900 + ty : 2000 + ty;\n",s:"(\\d{1,2})"};case"a":return{g:1,c:"if (results["+a+"] == 'am') {\nif (h == 12) { h = 0; }\n} else { if (h < 12) { h += 12; }}",s:"(am|pm)"};case"A":return{g:1,c:"if (results["+a+"] == 'AM') {\nif (h == 12) { h = 0; }\n} else { if (h < 12) { h += 12; }}",s:"(AM|PM)"};case"g":case"G":case"h":case"H":return{g:1,c:"h = parseInt(results["+a+"], 10);\n",s:"(\\d{1,2})"};case"i":return{g:1,c:"i = parseInt(results["+a+"], 10);\n",s:"(\\d{2})"};case"s":return{g:1,c:"s = parseInt(results["+a+"], 10);\n",s:"(\\d{2})"};case"O":return{g:0,c:null,s:"[+-]\\d{4}"};case"T":return{g:0,c:null,s:"[A-Z]{3}"};case"Z":return{g:0,c:null,s:"[+-]\\d{1,5}"};default:return{g:0,c:null,s:String.escape(b)};}};Date.prototype.getTimezone=function(){return this.toString().replace(/^.*? ([A-Z]{3}) [0-9]{4}.*$/,"$1").replace(/^.*?\(([A-Z])[a-z]+ ([A-Z])[a-z]+ ([A-Z])[a-z]+\)$/,"$1$2$3");};Date.prototype.getGMTOffset=function(){return(this.getTimezoneOffset()>0?"-":"+")+String.leftPad(Math.floor(Math.abs(this.getTimezoneOffset())/60),2,"0")+String.leftPad(Math.abs(this.getTimezoneOffset())%60,2,"0");};Date.prototype.getDayOfYear=function(){var a=0;Date.daysInMonth[1]=this.isLeapYear()?29:28;for(var b=0;b to your HTML Add class="sortable" to any table you'd like to make sortable Click on the headers to sort Thanks to many, many people for contributions and suggestions. Licenced as X11: http://www.kryogenix.org/code/browser/licence.html This basically means: do what you want with it. */ var stIsIE = /*@cc_on!@*/false; var up = ' ▴'; var down = ' ▾'; sorttable = { init: function() { // quit if this function has already been called if (arguments.callee.done) return; // flag this function so we don't do the same thing twice arguments.callee.done = true; // kill the timer if (_timer) clearInterval(_timer); if (!document.createElement || !document.getElementsByTagName) return; sorttable.DATE_RE = /^(\d\d?)[\/\.-](\d\d?)[\/\.-]((\d\d)?\d\d)$/; forEach(document.getElementsByTagName('table'), function(table) { if (table.className.search(/\bsortable\b/) != -1) { sorttable.makeSortable(table); } }); }, makeSortable: function(table) { if (table.getElementsByTagName('thead').length == 0) { // table doesn't have a tHead. Since it should have, create one and // put the first table row in it. the = document.createElement('thead'); the.appendChild(table.rows[0]); table.insertBefore(the,table.firstChild); } // Safari doesn't support table.tHead, sigh if (table.tHead == null) table.tHead = table.getElementsByTagName('thead')[0]; if (table.tHead.rows.length != 1) return; // can't cope with two header rows // Sorttable v1 put rows with a class of "sortbottom" at the bottom (as // "total" rows, for example). This is B&R, since what you're supposed // to do is put them in a tfoot. So, if there are sortbottom rows, // for backwards compatibility, move them to tfoot (creating it if needed). sortbottomrows = []; for (var i=0; i5' : down; this.appendChild(sortrevind); return; } if (this.className.search(/\bsorttable_sorted_reverse\b/) != -1) { // if we're already sorted by this column in reverse, just // re-reverse the table, which is quicker sorttable.reverse(this.sorttable_tbody); this.className = this.className.replace('sorttable_sorted_reverse', 'sorttable_sorted'); this.removeChild(document.getElementById('sorttable_sortrevind')); sortfwdind = document.createElement('span'); sortfwdind.id = "sorttable_sortfwdind"; sortfwdind.innerHTML = stIsIE ? ' 6' : up; this.appendChild(sortfwdind); return; } // remove sorttable_sorted classes theadrow = this.parentNode; forEach(theadrow.childNodes, function(cell) { if (cell.nodeType == 1) { // an element cell.className = cell.className.replace('sorttable_sorted_reverse',''); cell.className = cell.className.replace('sorttable_sorted',''); } }); sortfwdind = document.getElementById('sorttable_sortfwdind'); if (sortfwdind) { sortfwdind.parentNode.removeChild(sortfwdind); } sortrevind = document.getElementById('sorttable_sortrevind'); if (sortrevind) { sortrevind.parentNode.removeChild(sortrevind); } this.className += ' sorttable_sorted'; sortfwdind = document.createElement('span'); sortfwdind.id = "sorttable_sortfwdind"; sortfwdind.innerHTML = stIsIE ? ' 6' : up; this.appendChild(sortfwdind); // build an array to sort. This is a Schwartzian transform thing, // i.e., we "decorate" each row with the actual sort key, // sort based on the sort keys, and then put the rows back in order // which is a lot faster because you only do getInnerText once per row row_array = []; col = this.sorttable_columnindex; rows = this.sorttable_tbody.rows; for (var j=0; j 12) { // definitely dd/mm return sorttable.sort_ddmm; } else if (second > 12) { return sorttable.sort_mmdd; } else { // looks like a date, but we can't tell which, so assume // that it's dd/mm (English imperialism!) and keep looking sortfn = sorttable.sort_ddmm; } } } } return sortfn; }, getInnerText: function(node) { // gets the text we want to use for sorting for a cell. // strips leading and trailing whitespace. // this is *not* a generic getInnerText function; it's special to sorttable. // for example, you can override the cell text with a customkey attribute. // it also gets .value for fields. if (!node) return ""; hasInputs = (typeof node.getElementsByTagName == 'function') && node.getElementsByTagName('input').length; if (node.getAttribute("sorttable_customkey") != null) { return node.getAttribute("sorttable_customkey"); } else if (typeof node.textContent != 'undefined' && !hasInputs) { return node.textContent.replace(/^\s+|\s+$/g, ''); } else if (typeof node.innerText != 'undefined' && !hasInputs) { return node.innerText.replace(/^\s+|\s+$/g, ''); } else if (typeof node.text != 'undefined' && !hasInputs) { return node.text.replace(/^\s+|\s+$/g, ''); } else { switch (node.nodeType) { case 3: if (node.nodeName.toLowerCase() == 'input') { return node.value.replace(/^\s+|\s+$/g, ''); } case 4: return node.nodeValue.replace(/^\s+|\s+$/g, ''); break; case 1: case 11: var innerText = ''; for (var i = 0; i < node.childNodes.length; i++) { innerText += sorttable.getInnerText(node.childNodes[i]); } return innerText.replace(/^\s+|\s+$/g, ''); break; default: return ''; } } }, reverse: function(tbody) { // reverse the rows in a tbody newrows = []; for (var i=0; i=0; i--) { tbody.appendChild(newrows[i]); } delete newrows; }, /* sort functions each sort function takes two parameters, a and b you are comparing a[0] and b[0] */ sort_numeric: function(a,b) { aa = parseFloat(a[0].replace(/[^0-9.-]/g,'')); if (isNaN(aa)) aa = 0; bb = parseFloat(b[0].replace(/[^0-9.-]/g,'')); if (isNaN(bb)) bb = 0; return aa-bb; }, sort_alpha: function(a,b) { if (a[0]==b[0]) return 0; if (a[0] 0 ) { var q = list[i]; list[i] = list[i+1]; list[i+1] = q; swap = true; } } // for t--; if (!swap) break; for(var i = t; i > b; --i) { if ( comp_func(list[i], list[i-1]) < 0 ) { var q = list[i]; list[i] = list[i-1]; list[i-1] = q; swap = true; } } // for b++; } // while(swap) } } /* ****************************************************************** Supporting functions: bundled here to avoid depending on a library ****************************************************************** */ // Dean Edwards/Matthias Miller/John Resig /* for Mozilla/Opera9 */ if (document.addEventListener) { document.addEventListener("DOMContentLoaded", sorttable.init, false); } /* for Internet Explorer */ /*@cc_on @*/ /*@if (@_win32) document.write(" {% block js %} {% endblock %} {% block top %} {% endblock %}
{% block data %} {% endblock %}
================================================ FILE: silk/templates/silk/base/detail_base.html ================================================ {% extends 'silk/base/base.html' %} {% load static %} {% block style %} {% endblock %} {% block js %} {% endblock %} ================================================ FILE: silk/templates/silk/base/root_base.html ================================================ {% extends "silk/base/base.html" %} {% load silk_nav %} {% load silk_inclusion %} {% load static %} {% block body_class %} cbp-spmenu-push {% endblock %} {% block style %} {% endblock %} {% block filter %} {% endblock %} {% block top %} {% endblock %} {% block js %} {% endblock %} ================================================ FILE: silk/templates/silk/clear_db.html ================================================ {% extends 'silk/base/root_base.html' %} {% load silk_inclusion %} {% load static %} {% block pagetitle %}Silky - Clear DB{% endblock %} {% block menu %} {% root_menu request %} {% endblock %} {% block style %} {{ block.super }} {% endblock %} {% block js %} {{ block.super }} {% endblock %} {% block data %}

Silk Clear DB

{% csrf_token %}
{{ msg|linebreaks }}
{% endblock %} {# Hide filter hamburger menu #} {% block top %}{% endblock %} {% block filter %}{% endblock %} ================================================ FILE: silk/templates/silk/cprofile.html ================================================ {% extends "silk/base/detail_base.html" %} {% load silk_filters %} {% load silk_nav %} {% load silk_inclusion %} {% load static %} {% block pagetitle %}Silky - CProfile - {{ silk_request.path }}{% endblock %} {% block js %} {{ block.super }} {% endblock %} {% block style %} {{ block.super }} {% endblock %} {% block menu %} {% request_menu request silk_request %} {% endblock %} {% block data %}
{% if silk_request.pyprofile %}
CProfile
The below is a dump from the cPython profiler.
{% if silk_request.prof_file %} Click here to download profile. {% endif %}
{{ silk_request.pyprofile }}
{% endif %}
{% endblock %} ================================================ FILE: silk/templates/silk/inclusion/code.html ================================================
...
{% for line in code %}{{ line }}
{% endfor %}...
================================================ FILE: silk/templates/silk/inclusion/heading.html ================================================
{{ text }}
================================================ FILE: silk/templates/silk/inclusion/profile_menu.html ================================================ {% load silk_nav %} ================================================ FILE: silk/templates/silk/inclusion/profile_summary.html ================================================ {% load silk_filters %}
{{ profile.start_time | silk_date_time }}
{% if profile.name %}{{ profile.name }}{% elif profile.func_name %}{{ profile.func_name }}{% endif %}
{{ profile.path }}
{{ profile.time_taken|floatformat:"0" }}ms overall
{{ profile.time_spent_on_sql_queries|floatformat:"0" }}ms on queries
{{ profile.queries.count }} queries
================================================ FILE: silk/templates/silk/inclusion/request_menu.html ================================================ {% load silk_nav %} ================================================ FILE: silk/templates/silk/inclusion/request_summary.html ================================================ {% load silk_filters %}
{{ silk_request.start_time | silk_date_time }}
{% if silk_request.response.status_code %}{{ silk_request.response.status_code }} {% endif %}{{ silk_request.method }}
{{ silk_request.path }}
{{ silk_request.time_taken|floatformat:"0" }}ms overall{% if silk_request.total_meta_time %} +{{ silk_request.total_meta_time | floatformat:"0" }}ms{% endif %}
{{ silk_request.time_spent_on_sql_queries|floatformat:"0" }}ms on queries{% if silk_request.meta_time_spent_queries %} +{{ silk_request.meta_time_spent_queries | floatformat:"0" }}ms{% endif %}
{{ silk_request.num_sql_queries }} queries{% if silk_request.meta_num_queries %} +{{ silk_request.meta_num_queries }}{% endif %}
================================================ FILE: silk/templates/silk/inclusion/request_summary_row.html ================================================ {% load silk_filters %}
{{ silk_request.start_time | silk_date_time }}
{% if silk_request.response.status_code %}{{ silk_request.response.status_code }} {% endif %}{{ silk_request.method }}
{{ silk_request.path }}
{{ silk_request.time_taken|floatformat:"0" }}ms overall{% if silk_request.total_meta_time %} +{{ silk_request.total_meta_time | floatformat:"0" }}ms{% endif %}
{{ silk_request.time_spent_on_sql_queries|floatformat:"0" }}ms on queries{% if silk_request.meta_time_spent_queries %} +{{ silk_request.meta_time_spent_queries | floatformat:"0" }}ms{% endif %}
{{ silk_request.num_sql_queries }} queries{% if silk_request.meta_num_queries %} +{{ silk_request.meta_num_queries }}{% endif %}
================================================ FILE: silk/templates/silk/inclusion/root_menu.html ================================================ {% load silk_nav %} ================================================ FILE: silk/templates/silk/profile_detail.html ================================================ {% extends "silk/base/detail_base.html" %} {% load silk_filters %} {% load silk_nav %} {% load silk_inclusion %} {% load static %} {% block pagetitle %}Silky - Profile Detail - {{ silk_request.path }}{% endblock %} {% block js %} {{ block.super }} {% endblock %} {% block style %} {{ block.super }} {% endblock %} {% block menu %} {% profile_menu request profile silk_request %} {% endblock %} {% block data %}
{% profile_summary profile %}
{% if profile.file_path and profile.line_num %} {{ profile.file_path }}:{{ profile.line_num }}{% if profile.end_line_num %}:{{ profile.end_line_num }}{% endif %} {% else %} Location {% endif %}
Below shows where in your code this profile was defined. If your profile was defined dynamically (i.e in your settings.py), then this will show the range of lines that are covered by the profiling.
{% if code %}
{% code code actual_line %}
{% elif code_error %}
{{ code_error }}
{% endif %} {% if silk_request.prof_file %}
Profile graph
Below is a graph of the profile, with the nodes coloured by the time taken (red is more time). This should give a good indication of the slowest path through the profiled code.
Prune nodes taking up less than % of the total time
{% url 'silk:request_profile_dot' request_id=silk_request.pk as profile_dot_url %} {{ profile_dot_url|json_script:'profileDotURL' }} {% endif %} {% if silk_request.pyprofile %}
Python Profiler
The below is a dump from the cPython profiler.
{% if silk_request.prof_file %} Click here to download profile. {% endif %}
{{ silk_request.pyprofile }}
{% endif %}
{% endblock %} ================================================ FILE: silk/templates/silk/profiling.html ================================================ {% extends 'silk/base/root_base.html' %} {% load static %} {% load silk_inclusion %} {% block pagetitle %}Silky - Profiling - {{ silk_request.path }}{% endblock %} {% block menu %} {% if silk_request %} {% request_menu request silk_request %} {% else %} {% root_menu request %} {% endif %} {% endblock %} {% block style %} {{ block.super }} {% endblock %} {% block filter %}
{{ block.super }} {% endblock %} {% block js %} {{ block.super }} {% endblock %} {% block filters %}

Profile

Took longer than
milliseconds, executed more than
queries, and spent longer than
milliseconds executing queries.

Date Range

Executed
seconds ago, before
, and after
.

Function

{% endblock %} {% block data %} {% if results %}

Silk Profiler

{% for profile in results %} {% profile_summary profile %} {% endfor %} {% else %}

Silk Profiler

No Silk profiling was performed for this request. Please check that:
  • you use the @silk_profile decorator or with silk_profile(): context manager on the correct view
  • you have "silk" in INSTALLED_APPS
  • you have "silk.middleware.SilkyMiddleware" in MIDDLEWARE
  • you have SILKY_PYTHON_PROFILER set to True
{% endif %} {% endblock %} ================================================ FILE: silk/templates/silk/raw.html ================================================ {% load static %}
{{ body }}
================================================ FILE: silk/templates/silk/request.html ================================================ {% extends "silk/base/base.html" %} {% load silk_filters %} {% load silk_inclusion %} {% load static %} {% block pagetitle %}Silky - Request - {{ silk_request.path }}{% endblock %} {% block style %} {% endblock %} {% block js %} {% endblock %} {% block onload %} {% endblock %} {% block menu %} {% request_menu request silk_request %} {% endblock %} {% block data %}
{% request_summary silk_request %}
{% if query_params %} {% heading 'Query Parameters' %}
{{ query_params }}
{% endif %} {% heading 'Request Headers' %} {% for k,v in silk_request.headers.items %} {% endfor %}
{{ k.upper }} {{ v }}
{% if silk_request.raw_body %} {% heading 'Raw Request Body' %} {% if silk_request.raw_body|length > 1000 %} The raw request body is {{ silk_request.raw_body|length }} characters long and hence is too big to show here. Click here to view the raw request body. {% else %}
{{ silk_request.raw_body }}
{% endif %} {% endif %} {% if silk_request.body %} {% heading 'Request Body' %} {% if silk_request.body|length > 1000 %} The processed request body is {{ silk_request.body|length }} characters long and hence is too big to show here. Click here to view the request body. {% else %}
This is the body of the HTTP request represented as JSON for easier reading.
{{ silk_request.body }}
{% endif %} {% endif %} {% if silk_request.response.headers %} {% heading 'Response Headers' %} {% for k, v in silk_request.response.headers.items %} {% endfor %}
{{ k.upper }} {{ v }}
{% endif %} {% if silk_request.response.raw_body %} {% heading 'Raw Response Body' %} {% with raw_body=silk_request.response.raw_body_decoded %} {% if raw_body|length > 1000 %} The raw response body is {{ raw_body|length }} characters long and hence is too big to show here. Click here to view the raw response body. {% else %}
{{ raw_body }}
{% endif %} {% endwith %} {% endif %} {% if silk_request.response.body %} {% heading 'Response Body' %}
This is the body of the HTTP response represented as JSON for easier reading.
{% if silk_request.response.body|length > 1000 %} The response body is {{ silk_request.response.body|length }} characters long and hence is too big to show here. Click here to view the response body. {% else %}
{{ silk_request.response.body }}
{% endif %} {% endif %} {% if curl %} {% heading 'Curl' %}
Curl is a command-line utility for transferring data from servers. Paste the following into a terminal to repeat this request via command line.
{{ curl.strip }}
{% endif %} {% if client %} {% heading 'Django Test Client' %}
The following is working python code that makes use of the Django test client. It can be used to replicate this request from within a Django unit test, or simply as standalone Python.
{{ client.strip }}
{% endif %}
{% endblock %} ================================================ FILE: silk/templates/silk/requests.html ================================================ {% extends 'silk/base/root_base.html' %} {% load silk_inclusion %} {% load static %} {% block pagetitle %}Silky - Requests{% endblock %} {% block style %} {{ block.super }} {% endblock %} {% block menu %} {% root_menu request %} {% endblock %} {% block js %} {{ block.super }} {% endblock %} {% block filter %}
{{ block.super }} {% endblock %} > {% block filters %}

Request

Took longer than
milliseconds, executed more than
queries, and spent longer than
milliseconds executing queries.

Date Range

Executed
seconds ago, before
, and after
.

View

{% endblock %} {% block data %} {% if results %} {% if view_style == "row" %}
{% for silk_request in results %} {% request_summary_row silk_request %} {% endfor %}
{% else %} {% for silk_request in results %} {% request_summary silk_request %} {% endfor %} {% endif %} {% else %}

No matches found

No requests were found with current set of filters. Please alter your filters and try again.
{% endif %} {% endblock %} ================================================ FILE: silk/templates/silk/sql.html ================================================ {% extends 'silk/base/base.html' %} {% load silk_nav %} {% load silk_filters %} {% load static %} {% load silk_inclusion %} {% load silk_urls %} {% block pagetitle %}Silky - SQL - {{ silk_request.path }}{% endblock %} {% block js %} {{ block.super }} {% endblock %} {% block style %} {% endblock %} {% block filter %}
{{ block.super }} {% endblock %} {% block menu %} {% if profile %} {% profile_menu request profile silk_request %} {% elif silk_request %} {% request_menu request silk_request %} {% endif %} {% endblock %} {% block data %} {{ block.super }}
{% if profile or silk_request %}
{% endif %} {% if profile %} {% profile_summary profile %} {% endif %} {% if silk_request %} {% request_summary silk_request %} {% endif %} {% if profile or silk_request %}
{% endif %}
{% for sql_query in items %} {% sql_detail_url silk_request profile sql_query as detail_url %} {% endfor %}
At Action Tables Num. Joins Execution Time (ms)
+{{ sql_query.start_time_relative }} {{ sql_query.first_keywords }} {{ sql_query.tables_involved|join:", " }} {{ sql_query.num_joins }} {{ sql_query.time_taken | floatformat:6 }}
{% if items.paginator.num_pages > 1 %} {% endif %}
{% endblock %} ================================================ FILE: silk/templates/silk/sql_detail.html ================================================ {% extends "silk/base/detail_base.html" %} {% load static %} {% load silk_filters %} {% load silk_nav %} {% load silk_inclusion %} {% block pagetitle %}Silky - SQL Detail - {{ silk_request.path }}{% endblock %} {% block js %} {{ block.super }} {% endblock %} {% block style %} {{ block.super }} {% endblock %} {% block menu %} {% endblock %} {% block data %}
{{ sql_query.formatted_query|spacify|linebreaksbr }}
{{ sql_query.time_taken }}ms
{{ sql_query.num_joins }} joins
{% if analysis %}
Query Plan
{{ analysis | spacify | linebreaksbr }}
{% endif %}
Traceback
The below is the Python stacktrace that leads up the execution of the above SQL query. Use it to figure out where and why this SQL query is being executed and whether or not it's actually neccessary.
{% for ln in traceback %} {% if ln %}
{{ ln }}
{% if forloop.counter == pos %} {% code code actual_line %} {% endif %} {% endif %} {% endfor %}
{% endblock %} ================================================ FILE: silk/templates/silk/summary.html ================================================ {% extends 'silk/base/root_base.html' %} {% load silk_inclusion %} {% load static %} {% block menu %} {% root_menu request %} {% endblock %} {% block pagetitle %}Silky - Summary{% endblock %} {% block style %} {{ block.super }} {% endblock %} {% block js %} {{ block.super }} {% endblock %} {% block data %}
{% csrf_token %}
Using requests that executed seconds ago,
before ,
and after .

Summary

{% if num_requests %}
{{ num_requests }}
Requests
{{ num_profiles }}
Profiles
{{ avg_overall_time | floatformat:0 }}ms
Avg. Time
{{ avg_num_queries | floatformat:2 }}
Avg. #Queries
{{ avg_time_spent_on_queries |floatformat:0 }}ms
Avg. DB Time
{% else %}

No data

{% endif %}

Most Time Overall

{% if longest_queries_by_view %} {% for x in longest_queries_by_view %} {% request_summary x %} {% endfor %} {% else %}

No data

{% endif %}

Most Time Spent in Database

{% if most_time_spent_in_db %} {% for x in most_time_spent_in_db %} {% request_summary x %} {% endfor %} {% else %}

No data

{% endif %}

Most Database Queries

{% if most_queries %} {% for x in most_queries %} {% request_summary x %} {% endfor %} {% else %}

No data

{% endif %}
{% endblock %} {# Hide filter hamburger menu #} {% block top %}{% endblock %} {% block filter %}{% endblock %} ================================================ FILE: silk/templatetags/__init__.py ================================================ __author__ = 'mtford' ================================================ FILE: silk/templatetags/silk_filters.py ================================================ import re from django.template import Library from django.template.defaultfilters import stringfilter from django.utils import timezone from django.utils.html import conditional_escape from django.utils.safestring import mark_safe register = Library() def _no_op(x): return x def _esc_func(autoescape): if autoescape: return conditional_escape return _no_op @stringfilter def spacify(value, autoescape=None): esc = _esc_func(autoescape) val = esc(value).replace(' ', " ") val = val.replace('\t', ' ') return mark_safe(val) def _urlify(str): r = re.compile(r'"(?P.*\.py)", line (?P[0-9]+).*') m = r.search(str) while m: group = m.groupdict() src = group['src'] num = group['num'] start = m.start('src') end = m.end('src') rep = '{src}'.format(src=src, num=num) str = str[:start] + rep + str[end:] m = r.search(str) return str @register.filter def hash(h, key): return h[key] def _process_microseconds(dt_strftime): splt = dt_strftime.split('.') micro = splt[-1] time = '.'.join(splt[0:-1]) micro = '%.3f' % float('0.' + micro) return time + micro[1:] def _silk_date_time(dt): today = timezone.now().date() if dt.date() == today: dt_strftime = dt.strftime('%H:%M:%S.%f') return _process_microseconds(dt_strftime) else: return _process_microseconds(dt.strftime('%Y.%m.%d %H:%M.%f')) @register.filter(expects_localtime=True) def silk_date_time(dt): return _silk_date_time(dt) @register.filter def sorted(value): return sorted(value) @stringfilter def filepath_urlify(value, autoescape=None): value = _urlify(value) return mark_safe(value) @stringfilter def body_filter(value): print(value) if len(value) > 20: return 'Too big!' else: return value spacify.needs_autoescape = True filepath_urlify.needs_autoescape = True register.filter(spacify) register.filter(filepath_urlify) register.filter(body_filter) ================================================ FILE: silk/templatetags/silk_inclusion.py ================================================ from django.template import Library register = Library() def request_summary(silk_request): return {'silk_request': silk_request} def request_summary_row(silk_request): return {'silk_request': silk_request} def request_menu(request, silk_request): return {'request': request, 'silk_request': silk_request} def root_menu(request): return {'request': request} def profile_menu(request, profile, silk_request=None): context = {'request': request, 'profile': profile} if silk_request: context['silk_request'] = silk_request return context def profile_summary(profile): return {'profile': profile} def heading(text): return {'text': text} def code(lines, actual_line): return {'code': lines, 'actual_line': [x.strip() for x in actual_line]} register.inclusion_tag('silk/inclusion/request_summary.html')(request_summary) register.inclusion_tag('silk/inclusion/request_summary_row.html')(request_summary_row) register.inclusion_tag('silk/inclusion/profile_summary.html')(profile_summary) register.inclusion_tag('silk/inclusion/code.html')(code) register.inclusion_tag('silk/inclusion/request_menu.html')(request_menu) register.inclusion_tag('silk/inclusion/profile_menu.html')(profile_menu) register.inclusion_tag('silk/inclusion/root_menu.html')(root_menu) register.inclusion_tag('silk/inclusion/heading.html')(heading) ================================================ FILE: silk/templatetags/silk_nav.py ================================================ from django import template from django.urls import reverse register = template.Library() @register.simple_tag def navactive(request, urls, *args, **kwargs): path = request.path urls = [reverse(url, args=args) for url in urls.split()] if path in urls: cls = kwargs.get('class', None) if not cls: cls = "menu-item-selected" return cls return "" ================================================ FILE: silk/templatetags/silk_urls.py ================================================ from django.template import Library from django.urls import reverse register = Library() @register.simple_tag def sql_detail_url(silk_request, profile, sql_query): if profile and silk_request: return reverse( "silk:request_and_profile_sql_detail", args=[silk_request.id, profile.id, sql_query.id], ) elif profile: return reverse("silk:profile_sql_detail", args=[profile.id, sql_query.id]) elif silk_request: return reverse("silk:request_sql_detail", args=[silk_request.id, sql_query.id]) ================================================ FILE: silk/urls.py ================================================ from django.urls import path from silk.views.clear_db import ClearDBView from silk.views.cprofile import CProfileView from silk.views.profile_detail import ProfilingDetailView from silk.views.profile_dot import ProfileDotView from silk.views.profile_download import ProfileDownloadView from silk.views.profiling import ProfilingView from silk.views.raw import Raw from silk.views.request_detail import RequestView from silk.views.requests import RequestsView from silk.views.sql import SQLView from silk.views.sql_detail import SQLDetailView from silk.views.summary import SummaryView app_name = 'silk' urlpatterns = [ path(route='', view=SummaryView.as_view(), name='summary'), path(route='requests/', view=RequestsView.as_view(), name='requests'), path( route='request//', view=RequestView.as_view(), name='request_detail', ), path( route='request//sql/', view=SQLView.as_view(), name='request_sql', ), path( route='request//sql//', view=SQLDetailView.as_view(), name='request_sql_detail', ), path( route='request//raw/', view=Raw.as_view(), name='raw', ), path( route='request//pyprofile/', view=ProfileDownloadView.as_view(), name='request_profile_download', ), path( route='request//json/', view=ProfileDotView.as_view(), name='request_profile_dot', ), path( route='request//profiling/', view=ProfilingView.as_view(), name='request_profiling', ), path( route='request//profile//', view=ProfilingDetailView.as_view(), name='request_profile_detail', ), path( route='request//profile//sql/', view=SQLView.as_view(), name='request_and_profile_sql', ), path( route='request//profile//sql//', view=SQLDetailView.as_view(), name='request_and_profile_sql_detail', ), path( route='profile//', view=ProfilingDetailView.as_view(), name='profile_detail', ), path( route='profile//sql/', view=SQLView.as_view(), name='profile_sql', ), path( route='profile//sql//', view=SQLDetailView.as_view(), name='profile_sql_detail', ), path(route='profiling/', view=ProfilingView.as_view(), name='profiling'), path(route='cleardb/', view=ClearDBView.as_view(), name='cleardb'), path( route='request//cprofile/', view=CProfileView.as_view(), name='cprofile', ), ] ================================================ FILE: silk/utils/__init__.py ================================================ __author__ = 'mtford' ================================================ FILE: silk/utils/data_deletion.py ================================================ from django.conf import settings from django.db import connections def delete_model(model): engine = settings.DATABASES[model.objects.db]['ENGINE'] table = model._meta.db_table if 'mysql' in engine or 'postgresql' in engine: # Use "TRUNCATE" on the table with connections[model.objects.db].cursor() as cursor: if 'mysql' in engine: cursor.execute("SET FOREIGN_KEY_CHECKS=0;") cursor.execute(f"TRUNCATE TABLE {table}") cursor.execute("SET FOREIGN_KEY_CHECKS=1;") elif 'postgres' in engine: cursor.execute(f"ALTER TABLE {table} DISABLE TRIGGER USER;") cursor.execute(f"TRUNCATE TABLE {table} CASCADE") cursor.execute(f"ALTER TABLE {table} ENABLE TRIGGER USER;") return # Manually delete rows because sqlite does not support TRUNCATE and # oracle doesn't provide good support for disabling foreign key checks while True: items_to_delete = list( model.objects.values_list('pk', flat=True).all()[:800]) if not items_to_delete: break model.objects.filter(pk__in=items_to_delete).delete() ================================================ FILE: silk/utils/pagination.py ================================================ from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator __author__ = 'mtford' def _page(request, query_set, per_page=200): paginator = Paginator(query_set, per_page) page_number = request.GET.get('page') try: page = paginator.page(page_number) except PageNotAnInteger: page = paginator.page(1) except EmptyPage: page = paginator.page(paginator.num_pages) return page ================================================ FILE: silk/utils/profile_parser.py ================================================ import re _pattern = re.compile(' +') def parse_profile(output): """ Parse the output of cProfile to a list of tuples. """ if isinstance(output, str): output = output.split('\n') for i, line in enumerate(output): # ignore n function calls, total time and ordered by and empty lines line = line.strip() if i > 3 and line: columns = _pattern.split(line)[0:] function = ' '.join(columns[5:]) columns = columns[:5] + [function] yield columns ================================================ FILE: silk/views/__init__.py ================================================ __author__ = 'mtford' ================================================ FILE: silk/views/clear_db.py ================================================ import os import shutil from django.db import transaction from django.shortcuts import render from django.utils.decorators import method_decorator from django.views.generic import View from silk.auth import login_possibly_required, permissions_possibly_required from silk.config import SilkyConfig from silk.models import Profile, Request, Response, SQLQuery from silk.utils.data_deletion import delete_model @method_decorator(transaction.non_atomic_requests, name="dispatch") class ClearDBView(View): @method_decorator(login_possibly_required) @method_decorator(permissions_possibly_required) def get(self, request, *_, **kwargs): return render(request, 'silk/clear_db.html') @method_decorator(login_possibly_required) @method_decorator(permissions_possibly_required) def post(self, request, *_, **kwargs): context = {} if 'clear_all' in request.POST: delete_model(Profile) delete_model(SQLQuery) delete_model(Response) delete_model(Request) tables = ['Response', 'SQLQuery', 'Profile', 'Request'] context['msg'] = 'Cleared data for following silk tables: {}'.format(', '.join(tables)) if SilkyConfig().SILKY_DELETE_PROFILES: dir = SilkyConfig().SILKY_PYTHON_PROFILER_RESULT_PATH for files in os.listdir(dir): path = os.path.join(dir, files) try: shutil.rmtree(path) except OSError: os.remove(path) context['msg'] += '\nDeleted all profiles from the directory.' return render(request, 'silk/clear_db.html', context=context) ================================================ FILE: silk/views/code.py ================================================ from silk.config import SilkyConfig __author__ = 'mtford' def _code(file_path, line_num, end_line_num=None): line_num = int(line_num) if not end_line_num: end_line_num = line_num end_line_num = int(end_line_num) actual_line = [] lines = '' with open(file_path, encoding='utf-8') as f: r = range(max(0, line_num - 10), line_num + 10) for i, line in enumerate(f): if i in r: lines += line if i + 1 in range(line_num, end_line_num + 1): actual_line.append(line) code = lines.split('\n') return actual_line, code def _code_context(file_path, line_num, end_line_num=None, prefix=''): actual_line, code = _code(file_path, line_num, end_line_num) return { prefix + 'code': code, prefix + 'file_path': file_path, prefix + 'line_num': line_num, prefix + 'actual_line': actual_line } def _code_context_from_request(request, end_line_num=None, prefix=''): file_path = request.GET.get('file_path') line_num = request.GET.get('line_num') result = {} if file_path is not None and line_num is not None: result = _code_context(file_path, line_num, end_line_num, prefix) return result def _should_display_file_name(file_name): for ignored_file in SilkyConfig().SILKY_IGNORE_FILES: if ignored_file in file_name: return False return True ================================================ FILE: silk/views/cprofile.py ================================================ from django.shortcuts import render from django.utils.decorators import method_decorator from django.views.generic import View from silk.auth import login_possibly_required, permissions_possibly_required from silk.models import Request class CProfileView(View): @method_decorator(login_possibly_required) @method_decorator(permissions_possibly_required) def get(self, request, *_, **kwargs): request_id = kwargs['request_id'] silk_request = Request.objects.get(pk=request_id) context = { 'silk_request': silk_request, 'request': request} return render(request, 'silk/cprofile.html', context) ================================================ FILE: silk/views/profile_detail.py ================================================ from django.shortcuts import render from django.utils.decorators import method_decorator from django.views.generic import View from silk.auth import login_possibly_required, permissions_possibly_required from silk.models import Profile from silk.views.code import _code_context, _code_context_from_request class ProfilingDetailView(View): @method_decorator(login_possibly_required) @method_decorator(permissions_possibly_required) def get(self, request, *_, **kwargs): profile_id = kwargs['profile_id'] context = { 'request': request } profile = Profile.objects.get(pk=profile_id) file_path = profile.file_path line_num = profile.line_num context['pos'] = pos = int(request.GET.get('pos', 0)) if pos: context.update(_code_context_from_request(request, prefix='pyprofile_')) context['profile'] = profile context['line_num'] = file_path context['file_path'] = line_num context['file_column'] = 5 if profile.request: context['silk_request'] = profile.request if file_path and line_num: try: context.update(_code_context(file_path, line_num, profile.end_line_num)) except OSError as e: if e.errno == 2: context['code_error'] = e.filename + ' does not exist.' else: raise e return render(request, 'silk/profile_detail.html', context) ================================================ FILE: silk/views/profile_dot.py ================================================ # std import json import os import shutil import tempfile from contextlib import closing, contextmanager # 3rd party from io import StringIO from django.http import HttpResponse from django.shortcuts import get_object_or_404 from django.utils.decorators import method_decorator from django.views.generic import View from gprof2dot import DotWriter, PstatsParser, Theme # silk from silk.auth import login_possibly_required, permissions_possibly_required from silk.models import Request COLOR_MAP = Theme( mincolor=(0.18, 0.51, 0.53), maxcolor=(0.03, 0.49, 0.50), gamma=1.5, fontname='FiraSans', minfontsize=6.0, maxfontsize=6.0, ) @contextmanager def _temp_file_from_file_field(source): """ Create a temp file containing data from a django file field. """ source.open() with closing(source): try: with tempfile.NamedTemporaryFile(delete=False) as destination: shutil.copyfileobj(source, destination) yield destination.name finally: os.unlink(destination.name) def _create_profile(source, get_filename=_temp_file_from_file_field): """ Parse a profile from a django file field source. """ with get_filename(source) as filename: return PstatsParser(filename).parse() def _create_dot(profile, cutoff): """ Create a dot file from pstats data stored in a django file field. """ node_cutoff = cutoff / 100.0 edge_cutoff = 0.1 / 100.0 profile.prune(node_cutoff, edge_cutoff, [], False) with closing(StringIO()) as fp: DotWriter(fp).graph(profile, COLOR_MAP) return fp.getvalue() class ProfileDotView(View): @method_decorator(login_possibly_required) @method_decorator(permissions_possibly_required) def get(self, request, request_id): silk_request = get_object_or_404(Request, pk=request_id, prof_file__isnull=False) cutoff = float(request.GET.get('cutoff', '') or 5) profile = _create_profile(silk_request.prof_file) result = dict(dot=_create_dot(profile, cutoff)) return HttpResponse(json.dumps(result).encode('utf-8'), content_type='application/json') ================================================ FILE: silk/views/profile_download.py ================================================ from django.http import FileResponse from django.shortcuts import get_object_or_404 from django.utils.decorators import method_decorator from django.views.generic import View from silk.auth import login_possibly_required, permissions_possibly_required from silk.models import Request class ProfileDownloadView(View): @method_decorator(login_possibly_required) @method_decorator(permissions_possibly_required) def get(self, request, request_id): silk_request = get_object_or_404(Request, pk=request_id, prof_file__isnull=False) response = FileResponse(silk_request.prof_file) response['Content-Disposition'] = f'attachment; filename="{silk_request.prof_file.name}"' return response ================================================ FILE: silk/views/profiling.py ================================================ from django.db.models import Count, Sum from django.shortcuts import render from django.template.context_processors import csrf from django.utils.decorators import method_decorator from django.views.generic import View from silk.auth import login_possibly_required, permissions_possibly_required from silk.models import Profile, Request from silk.request_filters import BaseFilter, FiltersManager, filters_from_request class ProfilingView(View): show = [5, 10, 25, 100, 250] default_show = 25 order_by = ['Recent', 'Name', 'Function Name', 'Num. Queries', 'Time', 'Time on queries'] defualt_order_by = 'Recent' session_key_profile_filters = 'session_key_profile_filters' filters_manager = FiltersManager(session_key_profile_filters) def __init__(self, **kwargs): super().__init__(**kwargs) def _get_distinct_values(self, field, silk_request): if silk_request: query_set = Profile.objects.filter(request=silk_request) else: query_set = Profile.objects.all() function_names = [x[field] for x in query_set.values(field).distinct()] # Ensure top, default option is '' try: function_names.remove('') except ValueError: pass return [''] + function_names def _get_function_names(self, silk_request=None): return self._get_distinct_values('func_name', silk_request) def _get_names(self, silk_request=None): return self._get_distinct_values('name', silk_request) def _get_objects(self, show=None, order_by=None, name=None, func_name=None, silk_request=None, filters=None): if not filters: filters = [] if not show: show = self.default_show manager = Profile.objects if silk_request: query_set = manager.filter(request=silk_request) else: query_set = manager.all() if not order_by: order_by = self.defualt_order_by if order_by == 'Recent': query_set = query_set.order_by('-start_time') elif order_by == 'Name': query_set = query_set.order_by('-name') elif order_by == 'Function Name': query_set = query_set.order_by('-func_name') elif order_by == 'Num. Queries': query_set = query_set.annotate(num_queries=Count('queries')).order_by('-num_queries') elif order_by == 'Time': query_set = query_set.order_by('-time_taken') elif order_by == 'Time on queries': query_set = query_set.annotate(db_time=Sum('queries__time_taken')).order_by('-db_time') elif order_by: raise RuntimeError('Unknown order_by: "%s"' % order_by) if func_name: query_set = query_set.filter(func_name=func_name) if name: query_set = query_set.filter(name=name) for f in filters: query_set = f.contribute_to_query_set(query_set) query_set = query_set.filter(f) return list(query_set[:show]) def _create_context(self, request, *args, **kwargs): request_id = kwargs.get('request_id') if request_id: silk_request = Request.objects.get(pk=request_id) else: silk_request = None show = request.GET.get('show', self.default_show) order_by = request.GET.get('order_by', self.defualt_order_by) if show: show = int(show) func_name = request.GET.get('func_name', None) name = request.GET.get('name', None) filters = self.filters_manager.get(request) context = { 'show': show, 'order_by': order_by, 'request': request, 'func_name': func_name, 'options_show': self.show, 'options_order_by': self.order_by, 'options_func_names': self._get_function_names(silk_request), 'options_names': self._get_names(silk_request), 'filters': filters } context.update(csrf(request)) if silk_request: context['silk_request'] = silk_request if func_name: context['func_name'] = func_name if name: context['name'] = name objs = self._get_objects(show=show, order_by=order_by, func_name=func_name, silk_request=silk_request, name=name, filters=[BaseFilter.from_dict(x) for _, x in filters.items()]) context['results'] = objs return context @method_decorator(login_possibly_required) @method_decorator(permissions_possibly_required) def get(self, request, *args, **kwargs): return render(request, 'silk/profiling.html', self._create_context(request, *args, **kwargs)) @method_decorator(login_possibly_required) @method_decorator(permissions_possibly_required) def post(self, request): filters = filters_from_request(request) filters_as_dict = {ident: f.as_dict() for ident, f in filters.items()} self.filters_manager.save(request, filters_as_dict) return render(request, 'silk/profiling.html', self._create_context(request)) ================================================ FILE: silk/views/raw.py ================================================ import logging from django.http import HttpResponse from django.shortcuts import render from django.utils.decorators import method_decorator from django.views.generic import View from silk.auth import login_possibly_required, permissions_possibly_required from silk.models import Request Logger = logging.getLogger('silk.views.raw') class Raw(View): @method_decorator(login_possibly_required) @method_decorator(permissions_possibly_required) def get(self, request, request_id): typ = request.GET.get('typ', None) subtyp = request.GET.get('subtyp', None) body = None if typ and subtyp: silk_request = Request.objects.get(pk=request_id) if typ == 'request': body = silk_request.raw_body if subtyp == 'raw' else silk_request.body elif typ == 'response': Logger.debug(silk_request.response.raw_body_decoded) body = silk_request.response.raw_body_decoded if subtyp == 'raw' else silk_request.response.body return render(request, 'silk/raw.html', { 'body': body }) else: return HttpResponse(content='Bad Request', status=400) ================================================ FILE: silk/views/request_detail.py ================================================ import json from django.shortcuts import render from django.utils.decorators import method_decorator from django.views.generic import View from silk.auth import login_possibly_required, permissions_possibly_required from silk.code_generation.curl import curl_cmd from silk.code_generation.django_test_client import gen from silk.models import Request class RequestView(View): @method_decorator(login_possibly_required) @method_decorator(permissions_possibly_required) def get(self, request, request_id): silk_request = Request.objects.get(pk=request_id) query_params = None if silk_request.query_params: query_params = json.loads(silk_request.query_params) context = { 'silk_request': silk_request, 'query_params': json.dumps(query_params, sort_keys=True, indent=4) if query_params else None, 'request': request } if len(silk_request.raw_body) < 20000: # Don't do this for large request body = silk_request.raw_body try: body = json.loads(body) # Incase encoded as JSON except (ValueError, TypeError): pass context['curl'] = curl_cmd(url=request.build_absolute_uri(silk_request.path), method=silk_request.method, query_params=query_params, body=body, content_type=silk_request.content_type) context['client'] = gen(path=silk_request.path, method=silk_request.method, query_params=query_params, data=body, content_type=silk_request.content_type) return render(request, 'silk/request.html', context) ================================================ FILE: silk/views/requests.py ================================================ from django.db.models import Sum from django.shortcuts import render from django.template.context_processors import csrf from django.utils.decorators import method_decorator from django.views.generic import View from silk.auth import login_possibly_required, permissions_possibly_required from silk.models import Request, Response from silk.request_filters import BaseFilter, FiltersManager, filters_from_request __author__ = 'mtford' class RequestsView(View): show = [5, 10, 25, 100, 250] default_show = 25 order_by = { 'start_time': { 'label': 'Recent', 'additional_query_filter': None }, 'path': { 'label': 'Path', 'additional_query_filter': None }, 'num_sql_queries': { 'label': 'Num. Queries', 'additional_query_filter': None }, 'time_taken': { 'label': 'Time', 'additional_query_filter': lambda x: x.filter(time_taken__gte=0) }, 'db_time': { 'label': 'Time on queries', 'additional_query_filter': lambda x: x.annotate(db_time=Sum('queries__time_taken')) .filter(db_time__gte=0) }, } order_dir = { 'ASC': { 'label': 'Ascending' }, 'DESC': { 'label': 'Descending' } } view_style = { 'card': { 'label': 'Cards' }, 'row': { 'label': 'Rows' } } default_order_by = 'start_time' default_order_dir = 'DESC' default_view_style = 'card' session_key_request_filters = 'request_filters' filters_manager = FiltersManager(session_key_request_filters) @property def options_order_by(self): return [{'value': x, 'label': self.order_by[x]['label']} for x in self.order_by.keys()] @property def options_order_dir(self): return [{'value': x, 'label': self.order_dir[x]['label']} for x in self.order_dir.keys()] @property def options_view_style(self): return [{'value': x, 'label': self.view_style[x]['label']} for x in self.view_style.keys()] def _get_paths(self): return Request.objects.values_list( 'path', flat=True ).order_by( 'path' ).distinct() def _get_views(self): return Request.objects.values_list( 'view_name', flat=True ).exclude( view_name='' ).order_by( 'view_name' ).distinct() def _get_status_codes(self): return Response.objects.values_list( 'status_code', flat=True ).order_by( 'status_code' ).distinct() def _get_methods(self): return Request.objects.values_list( 'method', flat=True ).order_by( 'method' ).distinct() def _get_objects(self, show=None, order_by=None, order_dir=None, path=None, filters=None): if not filters: filters = [] if not show: show = self.default_show query_set = Request.objects.all() if not order_by: order_by = self.default_order_by if not order_dir: order_dir = self.default_order_dir if order_by not in self.order_by.keys(): raise RuntimeError('Unknown order_by: "%s"' % order_by) ob = self.order_by[order_by] if ob['additional_query_filter'] is not None: query_set = ob['additional_query_filter'](query_set) query_set = query_set.order_by('{}{}'.format('-' if order_dir == 'DESC' else '', order_by)) if path: query_set = query_set.filter(path=path) for f in filters: query_set = f.contribute_to_query_set(query_set) query_set = query_set.filter(f) return query_set[:show] def _create_context(self, request): raw_filters = self.filters_manager.get(request).copy() show = raw_filters.pop('show', self.default_show) order_by = raw_filters.pop('order_by', self.default_order_by) order_dir = raw_filters.pop('order_dir', self.default_order_dir) view_style = raw_filters.pop('view_style', self.default_view_style) if show: show = int(show) path = request.GET.get('path', None) context = { 'show': show, 'order_by': order_by, 'order_dir': order_dir, 'view_style': view_style, 'request': request, 'options_show': self.show, 'options_order_by': self.options_order_by, 'options_order_dir': self.options_order_dir, 'options_view_style': self.options_view_style, 'options_paths': self._get_paths(), 'options_status_codes': self._get_status_codes(), 'options_methods': self._get_methods(), 'view_names': self._get_views(), 'filters': raw_filters, } context.update(csrf(request)) if path: context['path'] = path context['results'] = self._get_objects(show, order_by, order_dir, path, filters=[BaseFilter.from_dict(x) for _, x in raw_filters.items()]) return context @method_decorator(login_possibly_required) @method_decorator(permissions_possibly_required) def get(self, request): # Retain filters and ordering if they were modified by GET params if request.GET: filters = { # filters from previous session **self.filters_manager.get(request), # new filters from GET, overriding old **{k: v for k, v in request.GET.items() if k in ['show', 'order_by', 'order_dir', 'view_style']}, } self.filters_manager.save(request, filters) return render(request, 'silk/requests.html', self._create_context(request)) @method_decorator(login_possibly_required) @method_decorator(permissions_possibly_required) def post(self, request): previous_session = self.filters_manager.get(request) filters = { # filters from previous session but only GET values **{k: v for k, v in previous_session.items() if k in ['show', 'order_by', 'order_dir', 'view_style']}, # new filters from POST, overriding old **{ident: f.as_dict() for ident, f in filters_from_request(request).items()}, } self.filters_manager.save(request, filters) return render(request, 'silk/requests.html', self._create_context(request)) ================================================ FILE: silk/views/sql.py ================================================ from django.shortcuts import render from django.utils.decorators import method_decorator from django.views.generic import View from silk.auth import login_possibly_required, permissions_possibly_required from silk.models import Profile, Request, SQLQuery from silk.utils.pagination import _page __author__ = 'mtford' class SQLView(View): page_sizes = [5, 10, 25, 100, 200, 500, 1000] default_page_size = 200 @method_decorator(login_possibly_required) @method_decorator(permissions_possibly_required) def get(self, request, *_, **kwargs): request_id = kwargs.get('request_id') profile_id = kwargs.get('profile_id') try: per_page = int(request.GET.get('per_page', self.default_page_size)) except (TypeError, ValueError): per_page = self.default_page_size context = { 'request': request, 'options_page_size': self.page_sizes, 'per_page': per_page, } if request_id: silk_request = Request.objects.get(id=request_id) query_set = SQLQuery.objects.filter(request=silk_request).order_by('-start_time') for q in query_set: q.start_time_relative = q.start_time - silk_request.start_time page = _page(request, query_set, per_page) context['silk_request'] = silk_request if profile_id: p = Profile.objects.get(id=profile_id) page = _page(request, p.queries.order_by('-start_time').all(), per_page) context['profile'] = p if not (request_id or profile_id): raise KeyError('no profile_id or request_id') # noinspection PyUnboundLocalVariable context['items'] = page return render(request, 'silk/sql.html', context) ================================================ FILE: silk/views/sql_detail.py ================================================ import os import re from django.core.exceptions import PermissionDenied from django.shortcuts import render from django.utils.decorators import method_decorator from django.utils.safestring import mark_safe from django.views.generic import View from silk.auth import login_possibly_required, permissions_possibly_required from silk.models import Profile, Request, SQLQuery from silk.views.code import _code class SQLDetailView(View): def _urlify(self, str): files = [] r = re.compile(r'"(?P.*\.py)", line (?P[0-9]+).*') m = r.search(str) n = 1 while m: group = m.groupdict() src = group['src'] files.append(src) num = group['num'] start = m.start('src') end = m.end('src') rep = '{src}'.format( pos=n, src=src, num=num, name='c%d' % n, ) str = str[:start] + rep + str[end:] m = r.search(str) n += 1 return str, files @method_decorator(login_possibly_required) @method_decorator(permissions_possibly_required) def get(self, request, *_, **kwargs): sql_id = kwargs.get('sql_id', None) request_id = kwargs.get('request_id', None) profile_id = kwargs.get('profile_id', None) sql_query = SQLQuery.objects.get(pk=sql_id) pos = int(request.GET.get('pos', 0)) file_path = request.GET.get('file_path', '') line_num = int(request.GET.get('line_num', 0)) tb = sql_query.traceback_ln_only analysis = sql_query.analysis str, files = self._urlify(tb) if file_path and file_path not in files: raise PermissionDenied tb = [mark_safe(x) for x in str.split('\n')] context = { 'sql_query': sql_query, 'traceback': tb, 'pos': pos, 'line_num': line_num, 'file_path': file_path, 'analysis': analysis, 'virtualenv_path': os.environ.get('VIRTUAL_ENV') or '', } if request_id: context['silk_request'] = Request.objects.get(pk=request_id) if profile_id: context['profile'] = Profile.objects.get(pk=int(profile_id)) if pos and file_path and line_num: actual_line, code = _code(file_path, line_num) context['code'] = code context['actual_line'] = actual_line return render(request, 'silk/sql_detail.html', context) ================================================ FILE: silk/views/summary.py ================================================ from django.db.models import Avg, Count, Max, Sum from django.shortcuts import render from django.template.context_processors import csrf from django.utils.decorators import method_decorator from django.views.generic import View from silk import models from silk.auth import login_possibly_required, permissions_possibly_required from silk.request_filters import BaseFilter, FiltersManager, filters_from_request class SummaryView(View): filters_key = 'summary_filters' filters_manager = FiltersManager(filters_key) def _avg_num_queries(self, filters): queries__aggregate = models.Request.objects.filter(*filters).annotate(num_queries=Count('queries')).aggregate(num=Avg('num_queries')) return queries__aggregate['num'] def _avg_time_spent_on_queries(self, filters): taken__aggregate = models.Request.objects.filter(*filters).annotate(time_spent=Sum('queries__time_taken')).aggregate(num=Avg('time_spent')) return taken__aggregate['num'] def _avg_overall_time(self, filters): taken__aggregate = models.Request.objects.filter(*filters).annotate(time_spent=Sum('time_taken')).aggregate(num=Avg('time_spent')) return taken__aggregate['num'] # TODO: Find a more efficient way to do this. Currently has to go to DB num. views + 1 times and is prob quite expensive def _longest_query_by_view(self, filters): values_list = models.Request.objects.filter(*filters).values_list("view_name").annotate(max=Max('time_taken')).filter(max__isnull=False).order_by('-max')[:5] requests = [] for view_name, _ in values_list: request = models.Request.objects.filter(view_name=view_name, *filters).filter(time_taken__isnull=False).order_by('-time_taken')[0] requests.append(request) return sorted(requests, key=lambda item: item.time_taken, reverse=True) def _time_spent_in_db_by_view(self, filters): values_list = models.Request.objects.filter(*filters).values_list('view_name').annotate(t=Sum('queries__time_taken')).filter(t__gte=0).order_by('-t')[:5] requests = [] for view, _ in values_list: r = models.Request.objects.filter(view_name=view, *filters).annotate(t=Sum('queries__time_taken')).filter(t__isnull=False).order_by('-t')[0] requests.append(r) return sorted(requests, key=lambda item: item.t, reverse=True) def _num_queries_by_view(self, filters): queryset = models.Request.objects.filter(*filters).values_list('view_name').annotate(t=Count('queries')).order_by('-t')[:5] views = [r[0] for r in queryset[:6]] requests = [] for view in views: try: r = models.Request.objects.filter(view_name=view, *filters).annotate(t=Count('queries')).order_by('-t')[0] requests.append(r) except IndexError: pass return sorted(requests, key=lambda item: item.t, reverse=True) def _create_context(self, request): raw_filters = self.filters_manager.get(request) filters = [BaseFilter.from_dict(filter_d) for _, filter_d in raw_filters.items()] avg_overall_time = self._avg_num_queries(filters) c = { 'request': request, 'num_requests': models.Request.objects.filter(*filters).count(), 'num_profiles': models.Profile.objects.filter(*filters).count(), 'avg_num_queries': avg_overall_time, 'avg_time_spent_on_queries': self._avg_time_spent_on_queries(filters), 'avg_overall_time': self._avg_overall_time(filters), 'longest_queries_by_view': self._longest_query_by_view(filters), 'most_time_spent_in_db': self._time_spent_in_db_by_view(filters), 'most_queries': self._num_queries_by_view(filters), 'filters': raw_filters } c.update(csrf(request)) return c @method_decorator(login_possibly_required) @method_decorator(permissions_possibly_required) def get(self, request): c = self._create_context(request) return render(request, 'silk/summary.html', c) @method_decorator(login_possibly_required) @method_decorator(permissions_possibly_required) def post(self, request): filters = {ident: f.as_dict() for ident, f in filters_from_request(request).items()} self.filters_manager.save(request, filters) return render(request, 'silk/summary.html', self._create_context(request)) ================================================ FILE: silk.sublime-project ================================================ { "folders": [{ "follow_symlinks": true, "path": ".", "folder_exclude_patterns": [ ".idea", "bower_components", "node_modules" ], "file_exclude_patterns": [ ".gitmodules", ".gitignore", "LICENSE" ] }] } ================================================ FILE: tox.ini ================================================ [gh-actions] python = 3.10: py310 3.11: py311 3.12: py312 3.13: py313 3.14: py314 [gh-actions:env] DJANGO = 4.2: dj42 5.1: dj51 5.2: dj52 6.0: dj60 main: djmain [tox] envlist = py{310,311,312,313,314}-dj{42,50,51,52}-{sqlite3,mysql,postgresql} py{312,313,314}-dj{60,main}-{sqlite3,mysql,postgresql} [testenv] usedevelop = True ignore_outcome = djmain: True changedir = {toxinidir}/project deps = -rrequirements.txt mysql: mysqlclient postgresql: psycopg2-binary dj42: django>=4.2,<4.3 dj51: django>=5.1,<5.2 dj52: django>=5.2,<5.3 dj60: django>=6.0,<6.1 djmain: https://github.com/django/django/archive/main.tar.gz py312: setuptools py313: setuptools py314: setuptools extras = formatting setenv = PYTHONPATH={toxinidir}:{toxinidir} PYTHONDONTWRITEBYTECODE=1 sqlite3: DB_ENGINE=sqlite3 sqlite3: DB_NAME=":memory:" mysql: DB_ENGINE=mysql mysql: DB_NAME=mysql mysql: DB_USER=root mysql: DB_PASSWORD=mysql mysql: DB_PORT=3306 postgresql: DB_ENGINE=postgresql postgresql: DB_NAME=postgres postgresql: DB_PASSWORD=postgres commands = pytest [flake8] ignore = E501, E203, W503