Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 21 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,29 @@ Check GET /api/v1/me/ for frontend feature gating.

---

🗺️ Database Schema Diagram

Generate an ER diagram image automatically from Django models:

```bash
python manage.py generate_schema_diagram
```

This command writes the diagram to:

- `docs/images/db_schema.png`

Then the README can show it directly:

![Database schema diagram](docs/images/db_schema.png)

The diagram is generated directly from Django model metadata and saved as a PNG image.

---

⚠️ Important notes

· core/settings.py currently uses development defaults (DEBUG=True, hardcoded DB credentials).
Switch to environment variables before production!
· Environment is loaded from `.env` (see `.env.sample`), including DB settings.
· Channel layer is in‑memory. Use Redis for production.
· Browsable API is enabled only when DEBUG=True.

Expand All @@ -143,4 +162,3 @@ Detailed contracts and checklists are in the docs/ folder:



!
102 changes: 102 additions & 0 deletions courses/management/commands/generate_schema_diagram.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
from math import ceil
from pathlib import Path

from django.apps import apps
from django.core.management import BaseCommand
from PIL import Image, ImageDraw, ImageFont


class Command(BaseCommand):
help = "Generate DB schema diagram image directly from Django models."

def add_arguments(self, parser):
parser.add_argument(
"--output",
default="docs/images/db_schema.png",
help="Output image path (default: docs/images/db_schema.png)",
)

def handle(self, *args, **options):
output = Path(options["output"])
output.parent.mkdir(parents=True, exist_ok=True)

model_specs = []
for app_label in ("accounts", "courses"):
app_config = apps.get_app_config(app_label)
for model in app_config.get_models():
fields = []
relations = []
for field in model._meta.get_fields():
if field.auto_created and not field.concrete:
continue
if field.is_relation:
rel_model = getattr(field, "related_model", None)
if rel_model is not None:
relations.append((field.name, rel_model.__name__))
if field.concrete:
fields.append(f"{field.name}: {field.__class__.__name__}")
elif field.concrete:
fields.append(f"{field.name}: {field.get_internal_type()}")
model_specs.append(
{
"name": model.__name__,
"app": app_label,
"fields": fields,
"relations": relations,
}
)

if not model_specs:
self.stdout.write(self.style.WARNING("No models found."))
return

font = ImageFont.load_default()
title_h = 20
line_h = 14
box_w = 360
box_gap_x = 48
box_gap_y = 28
max_field_lines = max(max(len(m["fields"]), 1) for m in model_specs)
box_h = title_h + (max_field_lines + 2) * line_h
cols = 3
rows = ceil(len(model_specs) / cols)
width = 48 + cols * box_w + (cols - 1) * box_gap_x + 48
height = 48 + rows * box_h + (rows - 1) * box_gap_y + 48

image = Image.new("RGB", (width, height), (250, 252, 255))
draw = ImageDraw.Draw(image)

positions = {}
for idx, spec in enumerate(model_specs):
row, col = divmod(idx, cols)
x = 48 + col * (box_w + box_gap_x)
y = 48 + row * (box_h + box_gap_y)
x2 = x + box_w
y2 = y + box_h
positions[spec["name"]] = (x, y, x2, y2)

fill = (236, 244, 255) if spec["app"] == "courses" else (239, 255, 244)
draw.rounded_rectangle((x, y, x2, y2), radius=10, fill=fill, outline=(80, 98, 130), width=2)
draw.text((x + 10, y + 6), f"{spec['name']} ({spec['app']})", font=font, fill=(25, 40, 60))
draw.line((x + 8, y + title_h, x2 - 8, y + title_h), fill=(130, 146, 170), width=1)

for i, field_line in enumerate(spec["fields"][: max_field_lines + 1]):
draw.text((x + 10, y + title_h + 6 + i * line_h), field_line, font=font, fill=(36, 50, 72))

for spec in model_specs:
sx1, sy1, sx2, sy2 = positions[spec["name"]]
start = (sx2, (sy1 + sy2) // 2)
for field_name, target_name in spec["relations"]:
target = positions.get(target_name)
if not target:
continue
tx1, ty1, tx2, ty2 = target
end = (tx1, (ty1 + ty2) // 2)
color = (90, 95, 110)
draw.line((start[0], start[1], end[0], end[1]), fill=color, width=2)
mx = (start[0] + end[0]) // 2 + 2
my = (start[1] + end[1]) // 2 - 10
draw.text((mx, my), field_name, font=font, fill=(70, 70, 80))

image.save(output.as_posix(), format="PNG")
self.stdout.write(self.style.SUCCESS(f"Schema diagram generated: {output.as_posix()}"))
1 change: 1 addition & 0 deletions docs/images/.gitkeep
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

Binary file added docs/images/db_schema.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading