simpleaudit-studio 0.2.6__tar.gz → 0.2.7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/.gitignore +4 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/PKG-INFO +34 -1
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/README.md +33 -0
- simpleaudit_studio-0.2.7/accounts/admin_urls.py +11 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/accounts/auth_urls.py +1 -0
- simpleaudit_studio-0.2.7/accounts/migrations/0003_project_archived.py +17 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/accounts/models.py +3 -0
- simpleaudit_studio-0.2.7/accounts/serializers.py +180 -0
- simpleaudit_studio-0.2.7/accounts/services.py +402 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/accounts/views.py +100 -8
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/audits/comparison.py +17 -17
- simpleaudit_studio-0.2.7/audits/migrations/0004_swap_endpoint_fks_for_models.py +111 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/audits/models.py +3 -3
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/audits/serializers.py +3 -3
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/audits/services.py +31 -25
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/audits/views.py +11 -10
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/config/settings.py +1 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/config/urls.py +20 -6
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/deploy/e2e_smoke.py +13 -19
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/deploy/seed_bulk.py +17 -16
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/docs/architecture.md +3 -2
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/docs/domain-model.md +33 -13
- simpleaudit_studio-0.2.7/docs/superpowers/specs/2026-09-26-super-admin-dashboard-design.md +254 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/e2e/test_ui.py +3 -3
- simpleaudit_studio-0.2.7/infra/context_processors.py +80 -0
- simpleaudit_studio-0.2.7/infra/management/commands/dev_server.py +116 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/management/commands/purge_test_data.py +2 -2
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/management/commands/seed_demo_audits.py +14 -13
- simpleaudit_studio-0.2.7/infra/management/commands/setup_local.py +66 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/seed.py +2 -14
- simpleaudit_studio-0.2.7/infra/seo.py +195 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/tests/factories.py +4 -14
- simpleaudit_studio-0.2.7/infra/tests/test_admin_api.py +173 -0
- simpleaudit_studio-0.2.7/infra/tests/test_admin_page.py +101 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/tests/test_api_e2e.py +27 -19
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/tests/test_audit_clone.py +15 -10
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/tests/test_audit_poll_results.py +7 -6
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/tests/test_audit_rename.py +2 -2
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/tests/test_audit_run_freeze.py +34 -34
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/tests/test_audit_sse.py +7 -6
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/tests/test_audit_submission.py +16 -13
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/tests/test_bootstrap.py +5 -6
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/tests/test_comparison.py +14 -11
- simpleaudit_studio-0.2.7/infra/tests/test_connection_delete.py +109 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/tests/test_engine_integration.py +14 -11
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/tests/test_health_panel.py +2 -3
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/tests/test_minimal_config.py +10 -8
- simpleaudit_studio-0.2.7/infra/tests/test_model_registry.py +47 -0
- simpleaudit_studio-0.2.7/infra/tests/test_profile.py +122 -0
- simpleaudit_studio-0.2.7/infra/tests/test_profile_ui.py +85 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/tests/test_purge_test_data.py +21 -15
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/tests/test_scenarios.py +0 -12
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/tests/test_seed_platform.py +2 -5
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/tests/test_smoke_all_pages.py +25 -8
- simpleaudit_studio-0.2.7/infra/tests/test_workspace_archive.py +120 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/tests/test_workspaces.py +20 -8
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/ui.py +271 -63
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/worker.py +117 -51
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/model_registry/admin.py +1 -8
- simpleaudit_studio-0.2.7/model_registry/migrations/0002_migrate_legacy_endpoints.py +53 -0
- simpleaudit_studio-0.2.7/model_registry/migrations/0003_remove_modeleendpoint.py +18 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/model_registry/models.py +5 -33
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/model_registry/serializers.py +0 -32
- simpleaudit_studio-0.2.7/model_registry/services.py +3 -0
- simpleaudit_studio-0.2.7/model_registry/urls.py +7 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/model_registry/views.py +1 -27
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/pyproject.toml +1 -1
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/scenarios/services.py +3 -1
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/scenarios/views.py +2 -1
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/simpleaudit_studio/cli.py +76 -8
- simpleaudit_studio-0.2.7/templates/admin.html +460 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/templates/audit_detail.html +3 -3
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/templates/base.html +25 -8
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/templates/dashboard.html +3 -3
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/templates/new_audit.html +38 -8
- simpleaudit_studio-0.2.7/templates/profile.html +134 -0
- simpleaudit_studio-0.2.7/templates/public/base_public.html +20 -0
- simpleaudit_studio-0.2.7/templates/public/landing.html +99 -0
- simpleaudit_studio-0.2.7/templates/seo/head.html +24 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/templates/workspaces.html +1 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/uv.lock +1 -1
- simpleaudit_studio-0.2.6/Makefile +0 -63
- simpleaudit_studio-0.2.6/accounts/serializers.py +0 -95
- simpleaudit_studio-0.2.6/accounts/services.py +0 -158
- simpleaudit_studio-0.2.6/infra/context_processors.py +0 -65
- simpleaudit_studio-0.2.6/infra/tests/test_model_registry.py +0 -89
- simpleaudit_studio-0.2.6/model_registry/services.py +0 -39
- simpleaudit_studio-0.2.6/model_registry/urls.py +0 -13
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/.dockerignore +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/.env.example +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/.env.local.example +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/.github/workflows/ci.yml +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/.github/workflows/publish.yml +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/Dockerfile +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/LICENSE +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/accounts/__init__.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/accounts/admin.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/accounts/apps.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/accounts/migrations/0001_initial.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/accounts/migrations/0002_user_workos_user_id.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/accounts/migrations/__init__.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/accounts/project_urls.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/accounts/workos_auth.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/audits/__init__.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/audits/admin.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/audits/apps.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/audits/events.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/audits/migrations/0001_initial.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/audits/migrations/0002_add_audit_run_archived.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/audits/migrations/0003_auditrun_updated_at.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/audits/migrations/__init__.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/audits/urls.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/config/__init__.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/config/asgi.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/config/wsgi.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/deploy/compose/Dockerfile +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/deploy/mock_openai_server.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/deploy/postgres-init.sql +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/docker-compose.yml +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/docs/deployment.md +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/__init__.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/admin.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/apps.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/engine.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/exceptions.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/fixtures/demo_audit_results.json +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/hashing.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/health.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/health_api.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/logging.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/management/__init__.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/management/commands/__init__.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/management/commands/bootstrap_platform.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/management/commands/run_worker.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/management/commands/seed_platform.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/middleware.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/minimal_config.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/permissions.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/readiness.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/simpleaudit_package.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/startup_checks.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/tests/__init__.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/tests/test_audit_archive.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/tests/test_audit_events.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/tests/test_audit_timing.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/tests/test_auth_projects.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/tests/test_health.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/tests/test_middleware.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/tests/test_scenario_import_export.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/tests/test_workspaces_ui.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/manage.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/model_registry/__init__.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/model_registry/apps.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/model_registry/migrations/0001_initial.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/model_registry/migrations/__init__.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/scenarios/__init__.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/scenarios/admin.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/scenarios/apps.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/scenarios/migrations/0001_initial.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/scenarios/migrations/__init__.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/scenarios/models.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/scenarios/serializers.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/scenarios/urls.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/simpleaudit_studio/__init__.py +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/static/favicon-16x16.svg +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/static/favicon-32x32.svg +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/static/favicon.svg +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/static/js/diff.min.js +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/static/logo.png +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/static/logo.svg +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/templates/404.html +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/templates/auth/login.html +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/templates/auth/register.html +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/templates/auth/workos_login.html +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/templates/auth/workos_verify.html +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/templates/compare.html +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/templates/health.html +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/templates/models.html +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/templates/scenario_result_detail.html +0 -0
- {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/templates/scenarios.html +0 -0
|
@@ -24,6 +24,7 @@ simpleaudit-dependency.yaml
|
|
|
24
24
|
.idea/
|
|
25
25
|
*.swp
|
|
26
26
|
.DS_Store
|
|
27
|
+
copilot-instructions.md
|
|
27
28
|
|
|
28
29
|
# Test/coverage
|
|
29
30
|
.coverage
|
|
@@ -37,5 +38,8 @@ docker-data/
|
|
|
37
38
|
*.log
|
|
38
39
|
logs/
|
|
39
40
|
|
|
41
|
+
# Runtime artifacts
|
|
42
|
+
app.pid
|
|
43
|
+
|
|
40
44
|
# Data artifacts (prototype)
|
|
41
45
|
data/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: simpleaudit-studio
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.7
|
|
4
4
|
Summary: Production AI model audit platform — one-liner demo mode via uvx
|
|
5
5
|
License: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -80,6 +80,39 @@ The pre-seeded connections point at a built-in mock server. To run real audits,
|
|
|
80
80
|
|
|
81
81
|
Set the API key in the connection form (leave blank for local servers that don't require auth), then launch an audit from the Scenario Library.
|
|
82
82
|
|
|
83
|
+
## 🛠️ Local Development
|
|
84
|
+
|
|
85
|
+
For contributors working on the codebase, use [uv](https://docs.astral.sh/uv/) — no manual venv steps, no Makefile. Two verbs cover everything:
|
|
86
|
+
|
|
87
|
+
- **`uv sync`** — create/update `.venv` from `pyproject.toml` + `uv.lock`
|
|
88
|
+
- **`uv run <cmd>`** — run any command inside `.venv` (auto-syncs first if the lockfile changed)
|
|
89
|
+
|
|
90
|
+
### Setup (one time)
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
git clone https://github.com/SushantGautam/SimpleAuditStudio
|
|
94
|
+
cd SimpleAuditStudio
|
|
95
|
+
cp .env.local.example .env # minimal local config (SQLite + mock models)
|
|
96
|
+
|
|
97
|
+
uv sync --extra dev --extra postgres # create .venv + install all deps
|
|
98
|
+
uv run manage.py setup_local # migrate + create admin (studio/admin123) + seed data
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`setup_local` chains the three idempotent first-run steps (migrate → bootstrap admin → seed scenario packs & model connections) into one command. Re-run it any time — it skips what already exists.
|
|
102
|
+
|
|
103
|
+
### Daily work
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
uv run manage.py dev_server --embedded # easiest: web UI + API + audit worker, zero-Docker → http://localhost:8000
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Other options:
|
|
110
|
+
```bash
|
|
111
|
+
uv run manage.py runserver # web UI + API only (no worker)
|
|
112
|
+
uv run manage.py dev_server # web UI + worker, uses .env (edit POSTGRES_*/HATCHET_* to point at your infra)
|
|
113
|
+
SIMPLEAUDIT_LOCAL_SQLITE=1 uv run manage.py test infra # tests
|
|
114
|
+
```
|
|
115
|
+
|
|
83
116
|
## 🐳 Self-Hosting
|
|
84
117
|
|
|
85
118
|
For teams or multi-user setups, use Docker Compose:
|
|
@@ -50,6 +50,39 @@ The pre-seeded connections point at a built-in mock server. To run real audits,
|
|
|
50
50
|
|
|
51
51
|
Set the API key in the connection form (leave blank for local servers that don't require auth), then launch an audit from the Scenario Library.
|
|
52
52
|
|
|
53
|
+
## 🛠️ Local Development
|
|
54
|
+
|
|
55
|
+
For contributors working on the codebase, use [uv](https://docs.astral.sh/uv/) — no manual venv steps, no Makefile. Two verbs cover everything:
|
|
56
|
+
|
|
57
|
+
- **`uv sync`** — create/update `.venv` from `pyproject.toml` + `uv.lock`
|
|
58
|
+
- **`uv run <cmd>`** — run any command inside `.venv` (auto-syncs first if the lockfile changed)
|
|
59
|
+
|
|
60
|
+
### Setup (one time)
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
git clone https://github.com/SushantGautam/SimpleAuditStudio
|
|
64
|
+
cd SimpleAuditStudio
|
|
65
|
+
cp .env.local.example .env # minimal local config (SQLite + mock models)
|
|
66
|
+
|
|
67
|
+
uv sync --extra dev --extra postgres # create .venv + install all deps
|
|
68
|
+
uv run manage.py setup_local # migrate + create admin (studio/admin123) + seed data
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`setup_local` chains the three idempotent first-run steps (migrate → bootstrap admin → seed scenario packs & model connections) into one command. Re-run it any time — it skips what already exists.
|
|
72
|
+
|
|
73
|
+
### Daily work
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
uv run manage.py dev_server --embedded # easiest: web UI + API + audit worker, zero-Docker → http://localhost:8000
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Other options:
|
|
80
|
+
```bash
|
|
81
|
+
uv run manage.py runserver # web UI + API only (no worker)
|
|
82
|
+
uv run manage.py dev_server # web UI + worker, uses .env (edit POSTGRES_*/HATCHET_* to point at your infra)
|
|
83
|
+
SIMPLEAUDIT_LOCAL_SQLITE=1 uv run manage.py test infra # tests
|
|
84
|
+
```
|
|
85
|
+
|
|
53
86
|
## 🐳 Self-Hosting
|
|
54
87
|
|
|
55
88
|
For teams or multi-user setups, use Docker Compose:
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from django.urls import path
|
|
2
|
+
|
|
3
|
+
from . import views
|
|
4
|
+
|
|
5
|
+
urlpatterns = [
|
|
6
|
+
path("stats/", views.admin_stats, name="admin-stats"),
|
|
7
|
+
path("workspaces/<int:project_id>/archive/", views.admin_archive_workspace, name="admin-workspace-archive"),
|
|
8
|
+
path("workspaces/<int:project_id>/unarchive/", views.admin_unarchive_workspace, name="admin-workspace-unarchive"),
|
|
9
|
+
path("users/", views.admin_create_user_view, name="admin-user-create"),
|
|
10
|
+
path("users/<int:user_id>/", views.admin_update_user_view, name="admin-user-update"),
|
|
11
|
+
]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Generated by Django 5.2.4 on 2026-09-26 08:58
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
dependencies = [
|
|
8
|
+
("accounts", "0002_user_workos_user_id"),
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
operations = [
|
|
12
|
+
migrations.AddField(
|
|
13
|
+
model_name="project",
|
|
14
|
+
name="archived",
|
|
15
|
+
field=models.BooleanField(db_index=True, default=False),
|
|
16
|
+
),
|
|
17
|
+
]
|
|
@@ -45,6 +45,9 @@ class Project(models.Model):
|
|
|
45
45
|
name = models.CharField(max_length=200, unique=True)
|
|
46
46
|
slug = models.SlugField(max_length=200, unique=True)
|
|
47
47
|
description = models.TextField(blank=True)
|
|
48
|
+
# Soft-hide a workspace that still contains data. Members keep read access;
|
|
49
|
+
# all mutations are blocked for non-superusers. Never deletes data.
|
|
50
|
+
archived = models.BooleanField(default=False, db_index=True)
|
|
48
51
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
49
52
|
updated_at = models.DateTimeField(auto_now=True)
|
|
50
53
|
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
from django.contrib.auth.password_validation import validate_password
|
|
2
|
+
from rest_framework import serializers
|
|
3
|
+
|
|
4
|
+
from accounts.models import Project, ProjectMembership, User
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class UserSerializer(serializers.ModelSerializer):
|
|
8
|
+
class Meta:
|
|
9
|
+
model = User
|
|
10
|
+
fields = ("id", "username", "email", "first_name", "last_name", "is_active")
|
|
11
|
+
read_only_fields = ("id",)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class RegisterSerializer(serializers.Serializer):
|
|
15
|
+
username = serializers.CharField(max_length=150)
|
|
16
|
+
email = serializers.EmailField()
|
|
17
|
+
password = serializers.CharField(write_only=True, style={"input_type": "password"})
|
|
18
|
+
first_name = serializers.CharField(required=False, allow_blank=True, default="")
|
|
19
|
+
last_name = serializers.CharField(required=False, allow_blank=True, default="")
|
|
20
|
+
|
|
21
|
+
def validate_username(self, value):
|
|
22
|
+
if User.objects.filter(username__iexact=value).exists():
|
|
23
|
+
raise serializers.ValidationError("Username already exists.")
|
|
24
|
+
return value
|
|
25
|
+
|
|
26
|
+
def validate_email(self, value):
|
|
27
|
+
if User.objects.filter(email__iexact=value).exists():
|
|
28
|
+
raise serializers.ValidationError("Email already exists.")
|
|
29
|
+
return value
|
|
30
|
+
|
|
31
|
+
def validate_password(self, value):
|
|
32
|
+
validate_password(value)
|
|
33
|
+
return value
|
|
34
|
+
|
|
35
|
+
def create(self, validated_data):
|
|
36
|
+
return User.objects.create_user(**validated_data)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ProjectSerializer(serializers.ModelSerializer):
|
|
40
|
+
class Meta:
|
|
41
|
+
model = Project
|
|
42
|
+
fields = ("id", "name", "slug", "description", "created_at", "updated_at")
|
|
43
|
+
read_only_fields = ("id", "created_at", "updated_at")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class WorkspaceItemSerializer(serializers.ModelSerializer):
|
|
47
|
+
"""Workspace as shown in lists/detail, including whether the requesting
|
|
48
|
+
user can administer it (drives UI affordances)."""
|
|
49
|
+
|
|
50
|
+
is_admin = serializers.SerializerMethodField()
|
|
51
|
+
|
|
52
|
+
class Meta:
|
|
53
|
+
model = Project
|
|
54
|
+
fields = ("id", "name", "slug", "description", "is_admin", "archived", "created_at", "updated_at")
|
|
55
|
+
read_only_fields = fields
|
|
56
|
+
|
|
57
|
+
def get_is_admin(self, obj) -> bool:
|
|
58
|
+
request = self.context.get("request")
|
|
59
|
+
user = getattr(request, "user", None)
|
|
60
|
+
if user is None or not user.is_authenticated:
|
|
61
|
+
return False
|
|
62
|
+
if user.is_superuser:
|
|
63
|
+
return True
|
|
64
|
+
return ProjectMembership.objects.filter(
|
|
65
|
+
project=obj, user=user, role=ProjectMembership.Role.ADMIN
|
|
66
|
+
).exists()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class WorkspaceCreateSerializer(serializers.Serializer):
|
|
70
|
+
name = serializers.CharField(max_length=200)
|
|
71
|
+
description = serializers.CharField(required=False, allow_blank=True, default="")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class WorkspaceUpdateSerializer(serializers.Serializer):
|
|
75
|
+
name = serializers.CharField(max_length=200, required=False)
|
|
76
|
+
description = serializers.CharField(required=False, allow_blank=True)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class MemberAddSerializer(serializers.Serializer):
|
|
80
|
+
username = serializers.CharField(max_length=150)
|
|
81
|
+
role = serializers.ChoiceField(choices=ProjectMembership.Role.choices, default=ProjectMembership.Role.VIEWER)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class MemberRoleSerializer(serializers.Serializer):
|
|
85
|
+
role = serializers.ChoiceField(choices=ProjectMembership.Role.choices)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# ─── Profile (self-service) ──────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class ProfileUpdateSerializer(serializers.Serializer):
|
|
92
|
+
"""Self-service profile update. All fields optional; only provided fields
|
|
93
|
+
are changed. Password change requires the current password."""
|
|
94
|
+
|
|
95
|
+
first_name = serializers.CharField(required=False, allow_blank=True, max_length=150)
|
|
96
|
+
last_name = serializers.CharField(required=False, allow_blank=True, max_length=150)
|
|
97
|
+
email = serializers.EmailField(required=False, allow_blank=True)
|
|
98
|
+
username = serializers.CharField(required=False, max_length=150)
|
|
99
|
+
current_password = serializers.CharField(required=False, style={"input_type": "password"}, write_only=True)
|
|
100
|
+
new_password = serializers.CharField(required=False, style={"input_type": "password"}, write_only=True)
|
|
101
|
+
|
|
102
|
+
def validate_username(self, value):
|
|
103
|
+
value = (value or "").strip()
|
|
104
|
+
if not value:
|
|
105
|
+
raise serializers.ValidationError("Username cannot be empty.")
|
|
106
|
+
if User.objects.filter(username__iexact=value).exclude(pk=self.context["user"].pk).exists():
|
|
107
|
+
raise serializers.ValidationError("Username already exists.")
|
|
108
|
+
return value
|
|
109
|
+
|
|
110
|
+
def validate_email(self, value):
|
|
111
|
+
value = (value or "").strip()
|
|
112
|
+
if value and User.objects.filter(email__iexact=value).exclude(pk=self.context["user"].pk).exists():
|
|
113
|
+
raise serializers.ValidationError("Email already exists.")
|
|
114
|
+
return value
|
|
115
|
+
|
|
116
|
+
def validate(self, attrs):
|
|
117
|
+
from accounts.services import has_local_password
|
|
118
|
+
|
|
119
|
+
user = self.context["user"]
|
|
120
|
+
if "new_password" in attrs:
|
|
121
|
+
if not attrs.get("new_password"):
|
|
122
|
+
raise serializers.ValidationError({"new_password": "Enter a new password."})
|
|
123
|
+
# SSO users (and anyone without a local password) have nothing to
|
|
124
|
+
# verify against, so they set a password directly.
|
|
125
|
+
if has_local_password(user):
|
|
126
|
+
if not attrs.get("current_password"):
|
|
127
|
+
raise serializers.ValidationError({"current_password": "Current password is required."})
|
|
128
|
+
if not user.check_password(attrs["current_password"]):
|
|
129
|
+
raise serializers.ValidationError({"current_password": "Current password is incorrect."})
|
|
130
|
+
validate_password(attrs["new_password"])
|
|
131
|
+
return attrs
|
|
132
|
+
|
|
133
|
+
def save(self):
|
|
134
|
+
user = self.context["user"]
|
|
135
|
+
attrs = self.validated_data
|
|
136
|
+
if "first_name" in attrs:
|
|
137
|
+
user.first_name = attrs["first_name"]
|
|
138
|
+
if "last_name" in attrs:
|
|
139
|
+
user.last_name = attrs["last_name"]
|
|
140
|
+
if "email" in attrs:
|
|
141
|
+
user.email = attrs["email"]
|
|
142
|
+
if "username" in attrs:
|
|
143
|
+
user.username = attrs["username"]
|
|
144
|
+
user.save()
|
|
145
|
+
if attrs.get("new_password"):
|
|
146
|
+
user.set_password(attrs["new_password"])
|
|
147
|
+
user.save(update_fields=["password"])
|
|
148
|
+
return user
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# ─── Super-admin user management ─────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class UserCreateSerializer(serializers.Serializer):
|
|
155
|
+
username = serializers.CharField(max_length=150)
|
|
156
|
+
email = serializers.EmailField(required=False, allow_blank=True)
|
|
157
|
+
password = serializers.CharField(write_only=True, style={"input_type": "password"})
|
|
158
|
+
first_name = serializers.CharField(required=False, allow_blank=True, default="")
|
|
159
|
+
last_name = serializers.CharField(required=False, allow_blank=True, default="")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class UserAdminUpdateSerializer(serializers.Serializer):
|
|
163
|
+
"""Super-admin edit of any user. All fields optional."""
|
|
164
|
+
|
|
165
|
+
first_name = serializers.CharField(required=False, allow_blank=True, max_length=150)
|
|
166
|
+
last_name = serializers.CharField(required=False, allow_blank=True, max_length=150)
|
|
167
|
+
email = serializers.EmailField(required=False, allow_blank=True)
|
|
168
|
+
is_active = serializers.BooleanField(required=False)
|
|
169
|
+
is_superuser = serializers.BooleanField(required=False)
|
|
170
|
+
password = serializers.CharField(required=False, write_only=True, style={"input_type": "password"})
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class ProjectMembershipSerializer(serializers.ModelSerializer):
|
|
174
|
+
username = serializers.CharField(source="user.username", read_only=True)
|
|
175
|
+
email = serializers.EmailField(source="user.email", read_only=True)
|
|
176
|
+
|
|
177
|
+
class Meta:
|
|
178
|
+
model = ProjectMembership
|
|
179
|
+
fields = ("id", "project", "user", "username", "email", "role", "created_at")
|
|
180
|
+
read_only_fields = ("id", "created_at")
|