django-snapnote
Let your users report a problem from any page: a floating button captures the tab, lets them draw on it and creates a ticket you can review in the admin or in built-in views.
Requires only Django (4.2–5.2) and Pillow. No DRF, no Node, no build step for you. The widget is a self-contained bundle rendered in a Shadow DOM, so your CSS and ours don't interfere.
Install
pip install django-snapnote
# settings.py
INSTALLED_APPS += ["snapnote"] # needs django.contrib.auth and staticfiles
# urls.py
path("snapnote/", include("snapnote.urls")), # report endpoint + management views
{# in your base template (the view needs the request context processor) #}
{% load snapnote_tags %}
{% snapnote_button %}
Then python manage.py migrate. Uploaded images use your MEDIA_ROOT/storage.
Settings
| Setting | Default | Purpose |
|---|---|---|
SNAPNOTE_CAN_REPORT |
"snapnote.permissions.is_authenticated" |
Dotted path to user -> bool: who sees the button and may report |
SNAPNOTE_NOTIFIERS |
["snapnote.notifiers.email"] |
Callables (ticket, absolute_url) run after a ticket is created |
SNAPNOTE_NOTIFY_EMAILS |
[] |
Recipients of the email notifier (nothing is sent if empty) |
SNAPNOTE_BASE_TEMPLATE |
None |
Template the management views extend; it only needs {% block content %} |
SNAPNOTE_ENABLE_MANAGEMENT_VIEWS |
True |
List/detail views (the admin is always registered) |
SNAPNOTE_SEARCH_UNACCENT |
False |
Accent-insensitive search (PostgreSQL + django.contrib.postgres only) |
SNAPNOTE_MAX_UPLOAD_MB |
5 |
Per-image size limit |
SNAPNOTE_MAX_DIMENSION |
8000 |
Max width/height in pixels |
SNAPNOTE_UPLOAD_TO |
"snapnote/" |
Prefix in the storage |
SNAPNOTE_THROTTLE |
"10/hour" |
Reports per user (second, minute, hour, day); uses Django's cache. None disables |
SNAPNOTE_AUTO_INJECT |
False |
Also add snapnote.middleware.AutoInjectMiddleware to MIDDLEWARE to insert the button automatically. Incompatible with strict CSP and streaming responses |
Customise the look by overriding CSS variables on #snapnote-root
(--snapnote-accent, --snapnote-accent-text, --snapnote-z-index, --snapnote-font).
Permissions
Reporting follows SNAPNOTE_CAN_REPORT. Managing uses the model permissions
snapnote.view_ticket, change_ticket and delete_ticket (superusers have them all).
Anonymous users are redirected to LOGIN_URL; authenticated users without permission get 403.
Notifiers
The signal snapnote.signals.ticket_created(sender, ticket, request_url) is sent after commit.
A notifier is any callable; a failing one is logged and never breaks the report:
# myproject/notifiers.py — Slack recipe
import requests
def slack(ticket, url):
requests.post(settings.SLACK_WEBHOOK, json={"text": f"New ticket: {ticket.title} {url}"}, timeout=5)
SNAPNOTE_NOTIFIERS = ["snapnote.notifiers.email", "myproject.notifiers.slack"]
Jira notifier
Built in, opt-in and dependency-free (stdlib only). It creates a Jira Cloud issue per ticket, with the page URL, a link to the Snapnote ticket and, optionally, the screenshot and annotations attached:
SNAPNOTE_NOTIFIERS = ["snapnote.notifiers.email", "snapnote.notifiers.jira_issue"]
SNAPNOTE_JIRA_URL = "https://yourcompany.atlassian.net"
SNAPNOTE_JIRA_EMAIL = "bot@yourcompany.com"
SNAPNOTE_JIRA_API_TOKEN = os.environ["JIRA_API_TOKEN"] # https://id.atlassian.com/manage-profile/security/api-tokens
SNAPNOTE_JIRA_PROJECT = "APP" # project key
# optional: SNAPNOTE_JIRA_ISSUE_TYPE = "Bug", SNAPNOTE_JIRA_LABELS = ["snapnote"],
# SNAPNOTE_JIRA_ATTACH_IMAGES = True, SNAPNOTE_JIRA_TIMEOUT = 10
manage.py check warns (snapnote.W002) if a required setting is missing. Use a dedicated
Jira account with access only to that project. Notifiers run synchronously after commit with the
timeout above; for heavy use, wrap it in your own task queue. Screenshots may contain sensitive
data: they are attached to the issue, so keep SNAPNOTE_JIRA_ATTACH_IMAGES = False if the Jira
project is broader than who may see them.
Security and deployment notes
- Screenshots can contain sensitive data. Serve
MEDIA_ROOTonly to authenticated staff (or use a private storage); do not expose it publicly. Purge old reports withpython manage.py snapnote_purge --older-than 90(--dry-runto preview). - Permissions-Policy: the browser needs
display-capture=(self). Withdisplay-capture=()(also sent by some proxies) capture never works and users get the upload fallback. - CSP: allow
img-src blob: data:andstyle-src 'unsafe-inline'(the Shadow DOM stylesheet is inline) andscript-srcfor the static bundle. - Uploads are validated (PNG/JPEG/WebP only, no SVG), size/dimension limited and re-encoded with Pillow.
page_urlmust be http(s) and is never rendered as a clickable link otherwise.- The report endpoint uses Django's CSRF protection; the template tag sets the cookie.
- Capture uses
getDisplayMedia(desktop browsers). On mobile the user attaches an image.
Development
pip install -e . pytest pytest-django
pytest # SQLite; SNAPNOTE_TEST_DB=postgres for PostgreSQL
cd frontend && npm ci && npm run build # writes src/snapnote/static/snapnote/snapnote.js
npm test && npm run e2e
The built snapnote.js is committed/packaged so installing the wheel never needs Node.
Metadata
Release files for django-snapnote 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| django_snapnote-0.2.0.tar.gz | 109.0 kB | With provenance | Details for django_snapnote-0.2.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| django_snapnote-0.2.0-py3-none-any.whl | Python 3 | none | any | Details for django_snapnote-0.2.0-py3-none-any.whl |
Total release size: 185.4 kB
Release files / django_snapnote-0.2.0.tar.gz
| Download URL | django_snapnote-0.2.0.tar.gz |
|---|---|
| Size | 109.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
7a01f7efd6360ad9eaa8e1f8a642d65825eae66b63547e5ef017b6291949df14
|
|
BLAKE2b-256 checksum How to use checksums |
cd5f25ffbc2809c46d6b5954d7c6a79ac95cb9946440ba37dbdb5a037241f650
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 12, 2026.
Transparency logRelease files / django_snapnote-0.2.0-py3-none-any.whl
| Download URL | django_snapnote-0.2.0-py3-none-any.whl |
|---|---|
| Size | 76.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
da7b2ab3c19770528ebc9dc43b0b05631abec9b01664d1f6f0b37ae32a9502bf
|
|
BLAKE2b-256 checksum How to use checksums |
1fa6ead533fc0b0b71ab6c9cf499e9208a85f3e9c4927b8f7b97abcdad94e650
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 12, 2026.
Transparency logSecurity for django-snapnote 0.2.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for django_snapnote-0.2.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 12, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.