Metadata-Version: 2.4
Name: django-test-doctor
Version: 0.2.2
Summary: 360° health check for Django projects — URLs, forms, admin, models, security, migrations in one command
Project-URL: Homepage, https://github.com/Altius-Academy-SNC/django-test-doctor
Project-URL: Documentation, https://altius-academy-snc.github.io/django-test-doctor
Project-URL: Repository, https://github.com/Altius-Academy-SNC/django-test-doctor
Project-URL: Issues, https://github.com/Altius-Academy-SNC/django-test-doctor/issues
Author-email: Paul Guindo <paulguindo@altius-group.ch>
License-Expression: MIT
License-File: LICENSE
Keywords: audit,django,healthcheck,linter,quality,smoke-test
Classifier: Development Status :: 3 - Alpha
Classifier: Framework :: Django
Classifier: Framework :: Django :: 4.2
Classifier: Framework :: Django :: 5.0
Classifier: Framework :: Django :: 5.1
Classifier: Framework :: Django :: 6.0
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Quality Assurance
Classifier: Topic :: Software Development :: Testing
Requires-Python: >=3.10
Requires-Dist: django>=4.2
Requires-Dist: rich>=13.0
Requires-Dist: tomli>=2.0; python_version < '3.11'
Provides-Extra: all
Requires-Dist: coverage>=7.0; extra == 'all'
Requires-Dist: djangorestframework>=3.14; extra == 'all'
Requires-Dist: jinja2>=3.1; extra == 'all'
Provides-Extra: coverage
Requires-Dist: coverage>=7.0; extra == 'coverage'
Provides-Extra: dev
Requires-Dist: coverage>=7.0; extra == 'dev'
Requires-Dist: djangorestframework>=3.14; extra == 'dev'
Requires-Dist: jinja2>=3.1; extra == 'dev'
Requires-Dist: pytest-cov; extra == 'dev'
Requires-Dist: pytest-django; extra == 'dev'
Requires-Dist: pytest>=7; extra == 'dev'
Requires-Dist: ruff; extra == 'dev'
Provides-Extra: docs
Requires-Dist: mkdocs-material; extra == 'docs'
Requires-Dist: mkdocstrings[python]; extra == 'docs'
Provides-Extra: drf
Requires-Dist: djangorestframework>=3.14; extra == 'drf'
Provides-Extra: html
Requires-Dist: jinja2>=3.1; extra == 'html'
Description-Content-Type: text/markdown

# django-test-doctor

[![PyPI](https://img.shields.io/pypi/v/django-test-doctor)](https://pypi.org/project/django-test-doctor/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue)](https://www.python.org/)
[![Django 4.2+](https://img.shields.io/badge/django-4.2%2B-green)](https://www.djangoproject.com/)

**360° health check for Django projects.** One command, twelve layers of analysis, one verdict.

Installed as `django-test-doctor`, imported as `django_doctor`:

```bash
pip install django-test-doctor
./manage.py doctor
```

## Why

`./manage.py check` validates runtime configuration. `djlint` validates template syntax.
`pytest` validates whatever you remembered to test. **Nothing validates the whole project at once.**

Real-world bug that slipped into production despite unit tests:

```python
class ContactPrincipalForm(forms.ModelForm):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # CRASH — Contact.fonction is a CharField (no choices)
        self.fields['fonction'].choices = [('', '---')] + list(self.fields['fonction'].choices)
```

The view using this form returned HTTP 500 in production. Any `./manage.py doctor` run
would have caught it, because the form check instantiates every `ModelForm` blank.

## Layers

| Layer         | What it does                                                              | Status |
|---------------|---------------------------------------------------------------------------|--------|
| `system`      | Wraps `./manage.py check` with rich output                                | ✅ 0.1 |
| `urls`        | Crawls `urlpatterns`, probes every URL as anon/user/staff/superuser       | ✅ 0.1 |
| `forms`       | Instantiates every `Form` / `ModelForm` blank + with empty data           | ✅ 0.1 |
| `migrations`  | `makemigrations --check --dry-run`, detects missing migrations            | ✅ 0.1 |
| `admin`       | `list_display` / `list_filter` / `search_fields` fields actually exist    | ✅ 0.2 |
| `models`      | `__str__` defined, `Meta.ordering`, FK `on_delete=SET_NULL` with `null=False` | ✅ 0.2 |
| `security`    | `check --deploy` + forbidden `SECRET_KEY` substrings + `DEBUG=True`       | ✅ 0.2 |
| `templates`   | `{% url %}`, `{% static %}`, context vars vs `get_context_data`           | 🚧 0.3 |
| `views`       | Permission declared, LoginRequired, has a test (via `coverage`)           | 🚧 0.3 |
| `drf`         | Serializer ↔ model fields match, OpenAPI diff vs `HEAD~1` for breaking    | 🚧 0.4 |
| `static`      | Every `{% static "x" %}` resolves to a file that exists                   | 🚧 0.4 |
| `i18n`        | Untranslated msgid, stale `.mo` vs `.po`, missing `_()` wrapping          | 🚧 0.4 |

## Quickstart

```bash
pip install django-test-doctor
```

Add to `INSTALLED_APPS`:

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

Then:

```bash
./manage.py doctor                       # full report
./manage.py doctor --section forms,urls  # target specific layers
./manage.py doctor --quick               # skip slow layers
./manage.py doctor --ci                  # exit 1 on critical/error
./manage.py doctor --html report.html    # browsable report
./manage.py doctor --diff origin/main    # only re-check what changed
./manage.py doctor --fix                 # auto-fix (run makemigrations, compilemessages)
./manage.py doctor --watch               # dev mode, re-run on save
```

## Configuration

In your project's `pyproject.toml`:

```toml
[tool.django-doctor]
enabled = ["*"]
disabled = ["drf"]
fail_on = ["critical", "error"]
ignore = ["urls:admin:*", "forms:legacy_migrations.*"]

[tool.django-doctor.urls]
roles = ["anonymous", "staff"]
skip = ["admin:*"]
timeout = 10
```

## Plugin API

Third-party checks plug in via the `django_doctor.checks` entry point:

```toml
[project.entry-points."django_doctor.checks"]
my_check = "my_package.checks:MyCheck"
```

```python
# my_package/checks.py
# note: imports use django_doctor — the distribution is django-test-doctor
from django_doctor import Check, Finding, Severity

class MyCheck(Check):
    id = "my_check"
    description = "Verify my custom invariant"

    def run(self, project):
        for thing in project.iter_something():
            if not thing.ok:
                yield Finding(
                    severity=Severity.ERROR,
                    location=thing.file_path,
                    message=f"{thing!r} is broken",
                    fix_hint="Run ./manage.py fix_things",
                )
```

## Status

Alpha — 0.2 ships seven layers (system / urls / forms / migrations / admin / models /
security). Roadmap in `CHANGELOG.md`.

## License

MIT © Paul Guindo
