Reusable django app that gives you a reason to say no. Reasons come from a bundled list or from a remote No-as-a-Service (NaaS) API, and can be rendered in templates or fetched from Python code.
Authored by Basil Shubin (who is available for freelance/contract work), and some great contributors.
First install the module, preferably in a virtual environment. It can be installed from PyPI:
pip install django-noRequires Python 3.11+ and Django 5.2+.
You'll need to add no to INSTALLED_APPS in your project's settings.py file:
INSTALLED_APPS += [
"no",
]There are no models, so no migrations are needed.
Load the no template tag library and use the {% no %} tag:
{% load no %}
<blockquote>{% no %}</blockquote>Or store the reason in a variable:
{% load no %}
{% no as reason %}
<p title="{{ reason }}">No.</p>Every {% no %} picks a new reason. Reasons are auto-escaped like any
other template output.
From Python code:
from no import get_reason
get_reason() # "I'm on a strict 'no commitments' diet."Or from the command line:
./manage.py noTell users why not on 403 pages, in your root URLconf:
handler403 = "no.views.permission_denied"Django has no handler429, so hand no.views.too_many_requests to your
rate limiter instead, e.g. django-ratelimit:
RATELIMIT_VIEW = "no.views.too_many_requests"Both render your own 403.html / 429.html if you have one, with
{{ reason }} and {{ exception }} in the context, and fall back to a
minimal built-in page otherwise. Reasons come from the configured backend;
if that's RemoteBackend, read the notes under RemoteBackend first.
Add a reason to every 403 and 429 JSON response:
pip install django-no[drf]REST_FRAMEWORK = {
"EXCEPTION_HANDLER": "no.contrib.rest_framework.exception_handler",
}{
"detail": "You do not have permission to perform this action.",
"reason": "I'm on a strict 'no commitments' diet."
}Reasons come from the configured backend, same as error pages above.
Already have a custom exception handler? Pass its response through
no.contrib.rest_framework.add_reason().
Everything is optional. Without any configuration reasons are picked at
random from the bundled list. To change that, set NO in your
settings.py:
NO = {
"BACKEND": "no.backends.local.LocalBackend",
"OPTIONS": {},
}BACKEND is the dotted path to a backend class and OPTIONS is passed
to it as keyword arguments.
The default. Picks a random reason from a JSON file containing a list of strings.
path- Path to your own reasons file. Defaults to the bundled
reasons.json.
NO = {
"BACKEND": "no.backends.local.LocalBackend",
"OPTIONS": {"path": BASE_DIR / "reasons.json"},
}Fetches a reason from an HTTP API that responds with
{"reason": "..."}. Whenever the request fails it falls back to
LocalBackend and stops calling the API for cooldown seconds.
url- API endpoint, must be
httporhttps. Defaults tohttps://naas.isalman.dev/no. timeout- Request timeout in seconds. Defaults to
3. cooldown- Seconds to wait before retrying the API after a failure. Defaults to
60. path- Reasons file for the fallback
LocalBackend.
NO = {
"BACKEND": "no.backends.remote.RemoteBackend",
"OPTIONS": {"timeout": 1, "cooldown": 300},
}Things to consider before using it:
- Every reason is an HTTP request made while the response is built, so a
slow API slows down your pages by up to
timeoutseconds. - The default public endpoint is rate limited to 120 requests per minute.
Past that it answers 429, and
RemoteBackendfalls back to local reasons forcooldownseconds. Fine for a hobby site; for anything busier, run your own no-as-a-service instance and pointurlat it, or stick withLocalBackend. - Error responses tend to come in floods, 429s especially. With error pages
or the REST framework handler enabled, every one of them is also a request
to
url. Make sure your endpoint can take it.
Subclass no.backends.base.BaseBackend and implement get_reason().
OPTIONS are available as self.options:
from no.backends.base import BaseBackend
class PoliteBackend(BaseBackend):
def get_reason(self):
return self.options.get("reason", "No, thank you.")Please see the example application. This application is used to
manually test the functionalities of this package. This also serves as
a good example.
If you've found a bug, implemented a feature or have a good reason to say no and think it is useful then please consider contributing. Patches, pull requests or just suggestions are welcome!
django-no is released under the MIT license.