django-launchpad
Renderer-independent navigation and application launchpads for Django.
Every Django application eventually needs navigation: a sidebar, a dashboard, a mobile menu, an account menu, an application launcher, or a command palette.
Most projects build each surface separately.
django-launchpad models them as different presentations of the same
navigation system.
- PyPI: https://pypi.org/project/django-launchpad/
- Source: https://github.com/fifoa-labs/django-launchpad
- License: MIT
Define destinations once. Compose them into named launchpads. Apply user-aware visibility. Resolve the result into a renderer-neutral tree. Then present that tree however your application needs.
NavigationLink
│
▼
LaunchpadNode
│
▼
Launchpad
│
▼
ResolvedLaunchpad
│
├── Sidebar
├── Dashboard cards
├── Top navigation
├── Mobile menu
├── Footer
└── Command palette
The package solves navigation structure and visibility. Your project owns the user interface.
Why Launchpad?
Navigation often begins as a few hard-coded links and gradually grows into duplicated logic spread across templates, context processors, views, permission checks, and frontend components.
Common problems follow:
- The same destination is defined repeatedly.
- Sidebars and dashboards drift out of sync.
- Permission checks differ between renderers.
- Active-state logic is duplicated.
- Project-specific templates become responsible for data loading.
- Changing navigation requires editing application code.
- A destination cannot be reused with different labels or placement rules.
Launchpad separates the problem into three durable concepts:
- NavigationLink — the canonical destination.
- LaunchpadNode — one placement of a destination or structural item.
- Launchpad — a named composition of nodes.
The same destination can appear in multiple launchpads with different titles, hierarchy, ordering, visibility, metadata, icons, and enabled states—without duplicating the destination itself.
Core Concepts
NavigationLink
A NavigationLink represents a reusable destination.
Examples:
- A Django named URL such as
reports:index - A relative path such as
/documentation/ - An external HTTPS URL
- A
mailto:ortel:link - A context-aware detail URL resolved at render time
A link defines canonical information such as:
- stable code
- title and short title
- description, tooltip, and ARIA label
- destination and URL arguments
- query parameters and fragment
- target,
rel, and download behavior - icon descriptor
- enabled state and disabled reason
- active-match strategy
- visibility policy
- search aliases and renderer-neutral metadata
A NavigationLink does not decide where it appears.
Launchpad
A Launchpad is a named, renderer-independent navigation composition.
Typical codes include:
primary_navigationhomepageaccount_menumobile_navigationreport_actionscommand_palette
Templates, tests, fixtures, and application code address a launchpad by its
stable code.
LaunchpadNode
A LaunchpadNode places something inside a launchpad tree.
A node can be:
- Link — a placement of a
NavigationLink - Section — a structural grouping node
- Separator — a structural divider
Nodes can be nested and ordered. A placement may override the linked destination's presentation without changing the canonical link.
Supported placement overrides include:
- code
- title and short title
- description and tooltip
- ARIA label
- call-to-action label
- icon
- enabled state and disabled reason
- visibility policy
- metadata
Link-level visibility and node-level visibility are both enforced.
This means the link can define a global minimum policy, while an individual placement may restrict visibility further.
Resolution Pipeline
get_launchpad() resolves stored configuration into a
ResolvedLaunchpad tree suitable for any renderer.
The reader:
- Loads the active launchpad by code.
- Loads active nodes and related navigation links.
- Builds one reusable visibility context for the user.
- Applies node visibility.
- Applies linked
NavigationLinkvisibility. - Resolves context-aware URLs.
- Applies placement overrides.
- Builds the parent-child tree.
- Removes empty sections.
- Removes leading, trailing, and duplicate separators.
- Computes active state for links and their ancestors.
- Returns renderer-neutral data.
Missing or inactive launchpads fail safely and return an empty
ResolvedLaunchpad rather than raising during template rendering.
Features
- Renderer-independent navigation architecture
- Reusable canonical destinations
- Named launchpad compositions
- Arbitrarily nested node trees
- Link, section, and separator nodes
- Placement-specific presentation overrides
- Public, authenticated, staff, superuser, and private audiences
- Explicit user and group visibility
- Django permission gates
allandanypermission modes- Scheduled visibility windows
- Registered runtime visibility rules
- Named Django URLs and validated raw URLs
- Positional and keyword URL arguments
- Query parameters and fragments
- Context-aware URL values
- Active matching by path or Django view name
- Disabled links that remain visible but non-navigable
- Renderer-neutral metadata
- Generic recursive Django template
- Django admin integration
- Fully typed package with
py.typed - Strict mypy validation
- 100% statement and branch coverage
Installation
Install from PyPI:
python -m pip install django-launchpad
With uv:
uv add django-launchpad
Add Launchpad to INSTALLED_APPS:
INSTALLED_APPS = [
# ...
"launchpad.apps.LaunchpadConfig",
]
Run migrations:
python manage.py migrate
No package settings are required for the default behavior.
Quick Start
Launchpad configuration can be created through Django admin, fixtures, migrations, the Django shell, or application code.
The following example creates one canonical destination and places it in a primary navigation launchpad.
from launchpad.models import Launchpad, LaunchpadNode, NavigationLink
reports_link = NavigationLink.objects.create(
code="reports",
title="Reports",
description="View operational reports.",
url_type=NavigationLink.URLType.NAMED,
url_value="reports:index",
audience=NavigationLink.Audience.AUTHENTICATED,
)
primary_navigation = Launchpad.objects.create(
code="primary_navigation",
title="Primary Navigation",
)
LaunchpadNode.objects.create(
launchpad=primary_navigation,
kind=LaunchpadNode.Kind.LINK,
navigation_link=reports_link,
audience=LaunchpadNode.Audience.PUBLIC,
sort_order=1000,
)
The node is public at the placement level, but the linked destination still requires an authenticated user. Both policies must pass.
Resolve in Python
from launchpad.readers import get_launchpad
navigation = get_launchpad(
"primary_navigation",
request=request,
)
The result is a ResolvedLaunchpad containing renderer-neutral
ResolvedNode objects.
Resolve in a Template
{% load launchpad_tags %}
{% get_launchpad "primary_navigation" as navigation %}
Render the bundled generic tree:
{% include "launchpad/generic/tree.html" with launchpad=navigation only %}
Or pass the same navigation object to a project-owned renderer.
Custom Rendering
Launchpad deliberately does not choose a visual framework.
The package does not require:
- Bootstrap
- Tailwind CSS
- Phoenix
- Bulma
- Material UI
- AdminLTE
- JavaScript navigation libraries
The generic template uses neutral launchpad-* classes and demonstrates
recursive rendering. It does not ship a theme, CSS, or JavaScript.
A consuming project may create any renderer it needs:
templates/
└── navigation/
├── sidebar.html
├── dashboard_cards.html
├── mobile_menu.html
└── command_palette.html
Example:
{% load launchpad_tags %}
{% get_launchpad "homepage" as homepage_navigation %}
{% include "navigation/dashboard_cards.html" with launchpad=homepage_navigation only %}
Renderers receive resolved data, not ORM query responsibilities.
Resolved Node Data
A ResolvedNode exposes values such as:
kindcodelink_codetitleshort_titledescriptiontooltiparia_labelcta_labelurltargetreldownloadiconenableddisabled_reasonis_activemetadatachildren
Convenience properties include:
is_linkis_sectionis_separatorhas_children
Visibility
Launchpad visibility controls whether a navigation object is shown.
It does not replace authorization in the destination view.
Destination views must continue to enforce their own permissions.
Audiences
Each visibility-aware link or node has a base audience:
publicauthenticatedstaffsuperuserprivate
Additional user, group, permission, schedule, and runtime-rule constraints may refine that policy.
Explicit Users and Groups
Private navigation can be granted to selected users or groups:
link.users.add(user)
link.groups.add(group)
Permission gates still apply when configured.
Permission Gates
Permissions use standard Django permission strings:
link.permissions_required = [
"reports.view_report",
"reports.export_report",
]
Require every permission:
link.permissions_mode = NavigationLink.PermissionMode.ALL
Require any one permission:
link.permissions_mode = NavigationLink.PermissionMode.ANY
Permissions are gates, not optional alternate grants. An explicitly assigned user still fails visibility when a configured permission gate fails.
Permission-only visibility is supported:
link.audience = NavigationLink.Audience.PRIVATE
link.permissions_required = ["reports.view_report"]
In this configuration, the permission itself may grant visibility.
Scheduled Visibility
Use visible_from and visible_until to make links or placements visible
only during a time window.
Inactive or scheduled-off objects remain hidden from everyone, including superusers.
Runtime Visibility Rules
Runtime rules support application-specific visibility that cannot be expressed through stored fields alone.
Register a rule under a safe code:
from launchpad.visibility import register_visibility_rule
@register_visibility_rule("has_reports_access")
def has_reports_access(*, obj, user, request, context) -> bool:
return bool(user and user.is_authenticated and context.get("reports_enabled"))
Store only the rule code:
link.visibility_rule = "has_reports_access"
Launchpad never stores or imports arbitrary Python paths from the database.
Missing rules and rule exceptions fail closed and hide the object.
URL Resolution
Named Django URLs
link.url_type = NavigationLink.URLType.NAMED
link.url_value = "people:detail"
link.url_kwargs = {"pk": 42}
Raw URLs
Supported raw URL forms include:
- relative paths
httphttpsmailtotel
Unsafe and unsupported values are rejected, including:
javascript:data:vbscript:- protocol-relative URLs
- unsupported schemes
#as a disabled-link substitute
Use enabled=False for disabled navigation.
Query Parameters and Fragments
link.query_params = {
"tab": "monthly",
"tag": ["finance", "operations"],
}
link.fragment = "summary"
Existing query parameters are preserved.
Context-Aware Values
URL arguments, keyword arguments, and query parameters may resolve values at runtime.
Supported roots:
@user@request@context
Example:
link.url_type = NavigationLink.URLType.NAMED
link.url_value = "people:detail"
link.url_kwargs = {
"pk": "@context.person.pk",
}
link.query_params = {
"next": "@request.path",
"viewer": "@user.username",
}
In a template:
{% get_launchpad "person_actions" person=person as navigation %}
Context traversal reads attributes and dictionary keys. Callables are not invoked. Missing values fail closed to an empty string.
Active Navigation
Links can determine whether they represent the current request.
Supported strategies:
autononeexact_pathpath_prefixview_nameview_prefix
auto prefers named-view matching for named URLs and then falls back to
path matching.
Configured query parameters also participate in active-state matching.
When a descendant is active, its resolved ancestors are marked active as well, allowing renderers to expand the appropriate sections.
Disabled Navigation
Disabled destinations may remain visible while becoming non-navigable.
link.enabled = False
link.disabled_reason = "Coming soon."
A placement can override the canonical enabled state:
node.enabled_override = False
node.disabled_reason_override = "Unavailable in this workspace."
Disabled nodes resolve to #, and the generic renderer emits
non-clickable markup with aria-disabled="true".
Icons
Launchpad stores renderer-neutral icon descriptors.
Supported descriptors:
noneemojifafe
Example:
link.icon_type = NavigationLink.IconType.EMOJI
link.emoji = "📊"
Or:
link.icon_type = NavigationLink.IconType.FA
link.icon_class = "fa-solid fa-chart-line"
The package does not bundle Font Awesome, Feather, or any other icon library. Renderers decide how descriptors are presented.
Django Admin
Launchpad registers all three models with Django admin:
NavigationLinkLaunchpadLaunchpadNode
The admin provides:
- structured fieldsets
- search and filtering
- related-object autocomplete
- inline node editing inside a launchpad
- direct node editing for larger trees
- resolved-value inspection
- automatic
created_byassignment - optimized changelist querysets
Django admin is a management interface, not a rendering requirement.
Public Python API
Models:
from launchpad.models import Launchpad, LaunchpadNode, NavigationLink
Reader:
from launchpad.readers import (
ResolvedLaunchpad,
ResolvedNode,
get_launchpad,
)
Visibility:
from launchpad.visibility import (
VisibilityContext,
build_user_context,
get_visibility_rule,
is_visible,
register_visibility_rule,
)
Template tag:
{% load launchpad_tags %}
{% get_launchpad "primary_navigation" as navigation %}
What Launchpad Does Not Do
Launchpad intentionally does not:
- replace authorization in Django views
- generate project views or URL patterns
- require a frontend framework
- ship an opinionated visual theme
- bundle CSS or JavaScript
- bundle icon libraries
- store executable Python import paths
- force navigation definitions into code or templates
- assume a specific user model
- require django-allauth
- require Django REST Framework
- require Redis or a particular cache backend
It is a focused Django application for navigation data, policy, resolution, and composition.
Custom User Models and django-allauth
User relationships use settings.AUTH_USER_MODEL.
Launchpad works with:
- Django's built-in user model
- custom
AbstractUsermodels - custom
AbstractBaseUsermodels - django-allauth with the project's configured user model
The visibility engine relies only on Django's standard authentication interface, including:
is_authenticatedis_staffis_superusergroupsget_all_permissions()
Supported Versions
| Python | Django 5.2 | Django 6.0 |
|---|---|---|
| 3.11 | Yes | No |
| 3.12 | Yes | Yes |
| 3.13 | Yes | Yes |
| 3.14 | Yes | Yes |
Package metadata currently allows:
Python >= 3.11
Django >= 5.2, < 6.1
Quality
django-launchpad is developed with the same quality standards used across
FIFOA Labs packages.
- Ruff formatting and linting
- Strict mypy validation across source and tests
django-stubs- Pytest and pytest-django
- 100% statement coverage
- 100% branch coverage
- CI across supported Python and Django combinations
- Source and wheel distribution validation
- Clean-wheel installation testing
- Typed distribution via
py.typed - PyPI trusted publishing
Project Status
The current version is 0.1.0.
django-launchpad is suitable for evaluation and integration, but its
public API is still pre-1.0 and may evolve as the package is adopted by
additional Django projects.
Semantic versioning is used:
- patch releases fix bugs and documentation
- minor
0.xreleases may add features or refine pre-1.0 APIs 1.0.0will mark a stable public compatibility commitment
Contributing
Issues and pull requests are welcome.
Before submitting changes, run:
make check
make coverage
make build
make check-dist
make install-wheel
See CONTRIBUTING.md for project guidelines.
License
django-launchpad is released under the MIT License.
See LICENSE for the full license text.
Built and maintained by FIFOA Labs.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file django_launchpad-0.1.0.tar.gz.
File metadata
- Download URL: django_launchpad-0.1.0.tar.gz
- Upload date:
- Size: 30.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8777f68892b20a2fb63962de31a8ae0be790949b3133841776eb07099ff4a844
|
|
| MD5 |
edc58b283da92b19d371a4ce4bbd203f
|
|
| BLAKE2b-256 |
ee259b9605406fb7ab2291c33984f935446ee1fbd0471ecd13f55ca5f3222f97
|
Provenance
The following attestation bundles were made for django_launchpad-0.1.0.tar.gz:
Publisher:
publish.yml on fifoa-labs/django-launchpad
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_launchpad-0.1.0.tar.gz -
Subject digest:
8777f68892b20a2fb63962de31a8ae0be790949b3133841776eb07099ff4a844 - Sigstore transparency entry: 2318682340
- Sigstore integration time:
-
Permalink:
fifoa-labs/django-launchpad@6f2403ca179669ad38164f4b3914c7af0df2caef -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/fifoa-labs
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@6f2403ca179669ad38164f4b3914c7af0df2caef -
Trigger Event:
release
-
Statement type:
File details
Details for the file django_launchpad-0.1.0-py3-none-any.whl.
File metadata
- Download URL: django_launchpad-0.1.0-py3-none-any.whl
- Upload date:
- Size: 38.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
280d02511a20e07300bbb5b517c9c20d6017621d50e401afcf5e482a8300967a
|
|
| MD5 |
54b8c5ec810d9896f9857e17157c2dfb
|
|
| BLAKE2b-256 |
532a93f312b1ad9cbf28858f551cbc4ef13b7e955c743df8adee047dbc6d21ba
|
Provenance
The following attestation bundles were made for django_launchpad-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on fifoa-labs/django-launchpad
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_launchpad-0.1.0-py3-none-any.whl -
Subject digest:
280d02511a20e07300bbb5b517c9c20d6017621d50e401afcf5e482a8300967a - Sigstore transparency entry: 2318682413
- Sigstore integration time:
-
Permalink:
fifoa-labs/django-launchpad@6f2403ca179669ad38164f4b3914c7af0df2caef -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/fifoa-labs
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@6f2403ca179669ad38164f4b3914c7af0df2caef -
Trigger Event:
release
-
Statement type: