Skip to main content

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_name attributes — read at runtime, so names inherited from a parent class in another file are found too
  • Generic-view default naming: a DetailView on Post correctly maps to blog/post_detail.html even with no template_name set
  • render() calls inside the class's own methods, tagged [in get()], [in post()], …
  • RedirectView subclasses via their pattern_name / url attributes

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.

Redirectsredirect(), 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 namesrender(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.json and git diff shows 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:

  1. Django's URL resolver — walked recursively, so include()s, prefixes, and namespaces resolve exactly the way Django resolves them at runtime;
  2. Runtime introspectioninspect.unwrap() to see through decorators, view_class to see through as_view(), class attributes for CBVs;
  3. Static AST analysis — each views.py is 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;
  4. 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 by return 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.partial fall 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django_uvtmap-0.1.0.tar.gz (16.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

django_uvtmap-0.1.0-py3-none-any.whl (18.6 kB view details)

Uploaded Python 3

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

Hashes for django_uvtmap-0.1.0.tar.gz
Algorithm Hash digest
SHA256 de039d3908b5b922f859cfe2df0b72fb86af81a75ec18fe3962ab2549844a88f
MD5 d2f1516603b86315381dd271006a4e92
BLAKE2b-256 53010cb86c1960b827b3a1cdff505e57f8538117cb5e60082a10d9958c1b1a26

See more details on using hashes here.

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

Hashes for django_uvtmap-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 b621368ea8dc55067296b6012c9ee314c24a33024f61f6e7e24aebd34e4c6b43
MD5 cc3c44643c1cee165f14d9cf5c340ad4
BLAKE2b-256 610ec75de895ed4a5ec82a7ece9deba5e380bbafa0f75c3ea32f95b5a3374fac

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page