G
YouTube TikTok Games
Skip to content
bashuPublic

About

❌ Reusable Django app that gives you a reason to say no

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

django-no

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.

Installation

First install the module, preferably in a virtual environment. It can be installed from PyPI:

pip install django-no

Requires Python 3.11+ and Django 5.2+.

Setup

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.

Usage

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 no

Error pages

Tell 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.

Django REST framework

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().

Configuration

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.

LocalBackend

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"},
}

RemoteBackend

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 http or https. Defaults to https://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 timeout seconds.
  • The default public endpoint is rate limited to 120 requests per minute. Past that it answers 429, and RemoteBackend falls back to local reasons for cooldown seconds. Fine for a hobby site; for anything busier, run your own no-as-a-service instance and point url at it, or stick with LocalBackend.
  • 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.

Custom backends

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.")

Example

Please see the example application. This application is used to manually test the functionalities of this package. This also serves as a good example.

Contributing

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!

License

django-no is released under the MIT license.

About

❌ Reusable Django app that gives you a reason to say no

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages