Visualize URL -> View -> Template relations in your Django project, with render conditions, redirects, and an interactive HTML map.
Project description
django-uvtmap
Visualize URL → View → Template relations in any Django project.
Ever stared at a Django codebase wondering which URL invokes which view, and which
template that view actually renders? uvtmap answers that in one command. It maps
every URL pattern to its view, every view to the templates it can render (including
the conditions under which each one is rendered), and every redirect back to its
target URL — then shows it all in an interactive, self-contained HTML visualizer.
Built for understanding large projects at a glance and for onboarding new developers.
/blog/<int:pk>/ blog.views.post_detail blog/post_detail_admin.html [if request.user.is_staff]
blog/post_detail.html
/blog/dashboard/ blog.views.dashboard blog/dash_compact.html [if mode == 'compact']
blog/dash_full.html [if not (mode == 'compact')]
blog/dash_default.html [on KeyError]
/blog/home/ blog.views.go_home => redirect to blog:list
Installation
pip install django-uvtmap
Add it to your project's INSTALLED_APPS (it only registers the management command —
no models, no migrations, no middleware):
INSTALLED_APPS = [
...
"uvtmap",
]
Development install (running from a clone of this repo):
pip install -e .
Quick start
python manage.py uvtmap # console table
python manage.py uvtmap --html map.html # interactive visualizer -> open in browser
python manage.py uvtmap --json map.json # machine-readable dump
Options
| Flag | What it does |
|---|---|
| (none) | Prints the table to the terminal, with a one-line summary at the end |
--html PATH |
Writes the interactive visualizer to PATH |
--json PATH |
Writes the raw map as JSON to PATH (schema below) |
--out PATH |
Writes the console table to a file instead of the terminal |
--app LABEL |
Only maps URLs whose view module (or URL namespace) belongs to that app |
--list-apps |
Lists every app with URL-routed views and how many, then exits |
--all |
Includes Django-internal and third-party views (admin, auth, packages) |
By default only views defined in your project are shown. Because --all output
can run to hundreds of lines, the command will offer to save the table to a file for
readability — press Enter at the prompt to print to the terminal anyway, or skip the
prompt entirely by passing --out yourself.
Not sure what to pass to --app? Run --list-apps first:
$ python manage.py uvtmap --list-apps
4 apps have URL-routed views:
blog 7 urls (project)
shop 4 urls (project)
allauth 15 urls (third-party)
django 64 urls (third-party)
Run with --app <name> to map just one app, e.g. --app allauth
Project apps are listed first, third-party apps after. This is a discovery step — it just reads module names off the URL resolver, so it never parses your source or checks templates and stays instant even on large projects.
The visualizer (--html)
The generated file is completely self-contained — no server, no internet, no build step. Open it in any browser, commit it to your repo, or send it to a teammate.
Three columns, wired together:
| Column | One node per… | Notes |
|---|---|---|
| urls | URL pattern | shows the pattern and its name= (namespaced) |
| views | unique view (module path + name) | two views named home in different apps stay distinct |
| templates | unique template name | a template used by five views appears once, with five wires |
Reading the wires
- solid grey — the template is always rendered by that view
- dashed — conditional (hover or click to see the exact condition)
- red — the view references a template that does not exist (yes, the tool doubles as a linter)
- dotted amber, pointing back to the URL column — the view redirects to that URL
Interactions
- Hover any node to light up its full chain, end to end
- Click a node to pin it and open the inspector: file paths, per-template conditions, redirect targets, and a source preview of the template itself
- Search (top right) filters to matching nodes plus everything connected to them, so chains never break apart
- Click the background to unpin
What it detects
Function-based views
render(),render_to_response(),TemplateResponse(),SimpleTemplateResponse()— as bare names or attribute calls (shortcuts.render(...))- Views wrapped in decorators (
@login_required,@cache_page, …) are unwrapped to their real source - Async views (
async def)
Class-based views
template_nameattributes — read at runtime, so names inherited from a parent class in another file are found too- Generic-view default naming: a
DetailViewonPostcorrectly maps toblog/post_detail.htmleven with notemplate_nameset render()calls inside the class's own methods, tagged[in get()],[in post()], …RedirectViewsubclasses via theirpattern_name/urlattributes
Conditions — the tool walks the AST upward from every render/redirect call and
records the branch path: if / elif / else (correctly negated), try/except
handlers, match/case, loops, and ternary expressions.
Redirects — redirect(), reverse(), reverse_lazy(), and literal-path
HttpResponseRedirect("/path/"), matched back to your URL patterns by name
(namespaced or bare) or by path.
Template verification — every statically-known template name is checked through
Django's actual configured template loaders. Missing ones are flagged [MISSING].
Dynamic names — render(request, f"blog/{theme}.html") can't be resolved
statically, so the expression itself is shown, marked (dynamic).
JSON output (--json)
Why JSON exists at all: the table is for humans and the visualizer is for exploring, but JSON is for machines. It lets the map feed into anything else:
- CI checks — fail the build if any template has
"exists": false, so broken template references never reach production; - Diffing — commit
map.jsonandgit diffshows exactly which routes, views, or templates a pull request touched; - Other tools — documentation generators, dead-template finders, custom dashboards, or your own visualizer if you outgrow the built-in one.
The output is a JSON array with one object per URL pattern:
{
"url": "/blog/<int:pk>/",
"url_name": "blog:detail",
"view": "blog.views.post_detail",
"view_file": "/path/to/blog/views.py",
"is_cbv": false,
"templates": [
{
"name": "blog/post_detail.html",
"static": true,
"conditions": ["if request.user.is_staff"],
"line": 18,
"exists": true,
"origin": "/path/to/blog/templates/blog/post_detail.html",
"source": "<h1>...</h1>"
}
],
"redirects": [
{ "target": "blog:list", "static": true, "conditions": [], "line": 24 }
]
}
Field reference
Top level — one per URL pattern
| Field | Type | Meaning |
|---|---|---|
url |
string | The full URL pattern, with all include() prefixes joined (e.g. /blog/<int:pk>/) |
url_name |
string / null | The pattern's name=, prefixed with its namespace(s) (blog:detail); null if unnamed |
view |
string | Dotted module path + view name — unique even if two apps have same-named views |
view_file |
string / null | Absolute path to the source file the view lives in; null for builtins |
is_cbv |
bool | true for class-based views, false for function-based |
templates |
array | Every template this view can render (see below); empty if it renders none |
redirects |
array | Every redirect this view can issue (see below); empty if it never redirects |
Template objects
| Field | Type | Meaning |
|---|---|---|
name |
string | The template name as written — or the expression if it's built at runtime |
static |
bool | true if the name is a literal string; false if computed (f-string, variable) |
conditions |
array of str | The branch path required to reach this render, outermost first (e.g. ["in get()", "if user.is_staff"]); empty = always rendered |
line |
int / null | Line number of the render call in view_file; null for CBV attributes / naming conventions |
exists |
bool / null | true = found by Django's template loaders; false = missing; null = dynamic name, not checkable |
origin |
string / null | Absolute path of the template file, as resolved by the loaders |
source |
string / null | The template's source text (truncated at 6 000 chars), used by the visualizer's preview |
Redirect objects
| Field | Type | Meaning |
|---|---|---|
target |
string | URL name (blog:list) or literal path (/blog/) being redirected to |
static |
bool | Same meaning as for templates |
conditions |
array of str | Branch path required for this redirect to fire |
line |
int / null | Line of the redirect()/reverse() call; null for RedirectView attributes |
How it works
No requests are made and none of your views are executed. The tool combines:
- Django's URL resolver — walked recursively, so
include()s, prefixes, and namespaces resolve exactly the way Django resolves them at runtime; - Runtime introspection —
inspect.unwrap()to see through decorators,view_classto see throughas_view(), class attributes for CBVs; - Static AST analysis — each
views.pyis parsed once (and cached); render and redirect calls are located, and a parent map of the AST lets the tool walk upward from each call to reconstruct the branch conditions; - Django's template loaders — to verify each template exists and find its file.
Known limitations
Honest list — pull requests welcome:
- Implicit fallthrough: in
if x: return render(a)followed byreturn render(b), template b is reported as "always" rather than "if not x". Fixing this needs control-flow analysis, not just AST walking. get_template_names()overrides in CBVs are not traced.- Views defined as lambdas or
functools.partialfall back to?. - Template names built at runtime (variables, f-strings) are shown as expressions, not resolved.
- Commented-out code is invisible — comments don't exist in the AST.
- Only the Django template backend is queried for existence checks (Jinja2 templates
resolve if configured in
TEMPLATES, but no Jinja2-specific parsing is done).
Requirements
- Python 3.9+ (uses
ast.unparse) - Django 4.2+
Acknowledgments
Developed in collaboration with Claude (Anthropic). The concept, original prototype, feature direction, and design decisions are by Abhinav Singh Bhagtana.
License
MIT
Project details
Release history Release notifications | RSS feed
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_uvtmap-0.1.0.tar.gz.
File metadata
- Download URL: django_uvtmap-0.1.0.tar.gz
- Upload date:
- Size: 16.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
de039d3908b5b922f859cfe2df0b72fb86af81a75ec18fe3962ab2549844a88f
|
|
| MD5 |
d2f1516603b86315381dd271006a4e92
|
|
| BLAKE2b-256 |
53010cb86c1960b827b3a1cdff505e57f8538117cb5e60082a10d9958c1b1a26
|
File details
Details for the file django_uvtmap-0.1.0-py3-none-any.whl.
File metadata
- Download URL: django_uvtmap-0.1.0-py3-none-any.whl
- Upload date:
- Size: 18.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b621368ea8dc55067296b6012c9ee314c24a33024f61f6e7e24aebd34e4c6b43
|
|
| MD5 |
cc3c44643c1cee165f14d9cf5c340ad4
|
|
| BLAKE2b-256 |
610ec75de895ed4a5ec82a7ece9deba5e380bbafa0f75c3ea32f95b5a3374fac
|