Metadata-Version: 2.4
Name: django-uvtmap
Version: 0.1.0
Summary: Visualize URL -> View -> Template relations in your Django project, with render conditions, redirects, and an interactive HTML map.
Project-URL: Homepage, https://github.com/Abhinvcodes/django-uvtmap
Project-URL: Issues, https://github.com/Abhinvcodes/django-uvtmap/issues
Author-email: Abhinav Singh Bhagtana <abhinav.bhagtana@outlook.com>
License: MIT
License-File: LICENSE
Keywords: django,documentation,onboarding,templates,urls,views,visualization
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Web Environment
Classifier: Framework :: Django
Classifier: Framework :: Django :: 4.2
Classifier: Framework :: Django :: 5.0
Classifier: Framework :: Django :: 5.1
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Software Development :: Documentation
Requires-Python: >=3.10
Requires-Dist: django>=4.2
Description-Content-Type: text/markdown

# 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

```bash
pip install django-uvtmap
```

Add it to your project's `INSTALLED_APPS` (it only registers the management command —
no models, no migrations, no middleware):

```python
INSTALLED_APPS = [
    ...
    "uvtmap",
]
```

Development install (running from a clone of this repo):

```bash
pip install -e .
```

## Quick start

```bash
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.

**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.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**:

```json
{
  "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 introspection** — `inspect.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
