Skip to content

Instantly share code, notes, and snippets.

@rg3915
Last active May 4, 2025 10:57
Show Gist options
  • Save rg3915/fbf200f7da048a6d52e1f0af1f7d91e5 to your computer and use it in GitHub Desktop.
Save rg3915/fbf200f7da048a6d52e1f0af1f7d91e5 to your computer and use it in GitHub Desktop.
Ninja Scaffold

Ninja Scaffold

A command-line tool for quickly generating Django applications with models, schemas, APIs, and admin interfaces using Django Ninja.

🇺🇸 English

Overview

Ninja Scaffold is a CLI utility that automates the creation of Django app components following best practices. It generates a complete structure for your Django models, including Ninja API endpoints, schema definitions, and admin interfaces.

Features

  • Create complete Django app structures with a single command
  • Generate models with customizable field types
  • Automatically create Django Ninja API endpoints (CRUD operations)
  • Generate proper ModelSchema definitions for input and output
  • Set up Django admin interfaces
  • Support for foreign keys and relationships between models
  • Generate additional files from existing models

Installation

  1. Download the ninja_scaffold.py script
  2. Place it in your Django project directory or add it to your PATH

Usage

Create a new model with fields

python ninja_scaffold.py <app_name> <model_name> [field:type_field]...

Example:

python ninja_scaffold.py crm Person name:charfield email:charfield age:integerfield

This creates:

- crm/__init__.py
- crm/admin.py (with PersonAdmin)
- crm/api.py (with CRUD endpoints)
- crm/apps.py
- crm/models.py (with Person model)
- crm/schemas.py (with PersonSchema and PersonInputSchema)

Generate files from an existing model

python ninja_scaffold.py --generate-from-model <app_name> <model_name>

Example:

python ninja_scaffold.py --generate-from-model crm Person

This reads an existing model and creates/updates:

- crm/admin.py
- crm/api.py
- crm/schemas.py

Supported Field Types

- charfield: CharField (max_length=100)
- textfield: TextField (null=True, blank=True)
- integerfield: IntegerField (null=True, blank=True)
- booleanfield: BooleanField (default=False)
- decimalfield: DecimalField (decimal_places=2, max_digits=7)
- datefield: DateField (null=True, blank=True)
- datetimefield: DateTimeField (auto_now_add=True)
- emailfield: EmailField (max_length=254)
- urlfield: URLField (max_length=200, null=True, blank=True)
- slugfield: SlugField (max_length=50)
- uuidfield: UUIDField (default=uuid.uuid4, editable=False)
- filefield: FileField (upload_to="uploads/", null=True, blank=True)
- imagefield: ImageField (upload_to="images/", null=True, blank=True)
- foreignkey: ForeignKey (Automatically links to a model based on the field name)
- manytomanyfield: ManyToManyField
- onetoone: OneToOneField
- jsonfield: JSONField (default=dict, null=True, blank=True)

Foreign Key Example

For relationship fields, the target model is derived from the field name:

python ninja_scaffold.py crm Provider name:charfield email:charfield person:foreignkey

This creates a ForeignKey field named "person" that references the "Person" model.

Output

The tool generates properly structured Django files with:

  • Models with appropriate field definitions
  • Ninja ModelSchema classes for serialization
  • Complete CRUD API endpoints with proper typing
  • Admin interface with sensible defaults

🇧🇷 Português

Visão Geral

Ninja Scaffold é uma ferramenta de linha de comando que automatiza a criação de componentes de aplicativos Django seguindo as melhores práticas. Ela gera uma estrutura completa para seus modelos Django, incluindo endpoints de API Ninja, definições de schema e interfaces de administração.

Características

  • Cria estruturas completas de aplicativos Django com um único comando
  • Gera modelos com tipos de campos personalizáveis
  • Cria automaticamente endpoints de API Django Ninja (operações CRUD)
  • Gera definições adequadas de ModelSchema para entrada e saída
  • Configura interfaces de administração Django
  • Suporte para chaves estrangeiras e relacionamentos entre modelos
  • Gera arquivos adicionais a partir de modelos existentes

Instalação

  1. Baixe o script ninja_scaffold.py
  2. Coloque-o no diretório do seu projeto Django ou adicione-o ao seu PATH

Uso

Criar um novo modelo com campos

python ninja_scaffold.py <nome_do_app> <nome_do_modelo> [campo:tipo_campo]...

Exemplo:

python ninja_scaffold.py crm Person name:charfield email:charfield age:integerfield

Isso cria:

- crm/__init__.py
- crm/admin.py (com PersonAdmin)
- crm/api.py (com endpoints CRUD)
- crm/apps.py
- crm/models.py (com o modelo Person)
- crm/schemas.py (com PersonSchema e PersonInputSchema)

Gerar arquivos a partir de um modelo existente

python ninja_scaffold.py --generate-from-model <nome_do_app> <nome_do_modelo>

Exemplo:

python ninja_scaffold.py --generate-from-model crm Person

Isso lê um modelo existente e cria/atualiza:

- crm/admin.py
- crm/api.py
- crm/schemas.py

Tipos de Campo Suportados

- charfield: CharField (max_length=100)
- textfield: TextField (null=True, blank=True)
- integerfield: IntegerField (null=True, blank=True)
- booleanfield: BooleanField (default=False)
- decimalfield: DecimalField (decimal_places=2, max_digits=7)
- datefield: DateField (null=True, blank=True)
- datetimefield: DateTimeField (auto_now_add=True)
- emailfield: EmailField (max_length=254)
- urlfield: URLField (max_length=200, null=True, blank=True)
- slugfield: SlugField (max_length=50)
- uuidfield: UUIDField (default=uuid.uuid4, editable=False)
- filefield: FileField (upload_to="uploads/", null=True, blank=True)
- imagefield: ImageField (upload_to="images/", null=True, blank=True)
- foreignkey: ForeignKey (Vincula automaticamente a um modelo com base no nome do campo)
- manytomanyfield: ManyToManyField
- onetoone: OneToOneField
- jsonfield: JSONField (default=dict, null=True, blank=True)

Exemplo de Chave Estrangeira

Para campos de relacionamento, o modelo alvo é derivado do nome do campo:

python ninja_scaffold.py crm Provider name:charfield email:charfield person:foreignkey

Isso cria um campo ForeignKey chamado "person" que referencia o modelo "Person".

Saída

A ferramenta gera arquivos Django adequadamente estruturados com:

  • Modelos com definições de campo apropriadas
  • Classes Ninja ModelSchema para serialização
  • Endpoints de API CRUD completos com tipagem adequada
  • Interface de administração com padrões sensatos
# api.py principal
from ninja import NinjaAPI, Redoc # noqa F401
# api = NinjaAPI()
api = NinjaAPI(docs=Redoc())
api.add_router('', 'apps.core.api.router')
# Adiciona mais apps aqui
# api.add_router('', 'apps.person.api.router')
#!/usr/bin/env python3
"""
ninja_scaffold: A CLI tool to quickly scaffold Django apps with models and APIs.
Usage:
python ninja_scaffold.py <app_name> <model_name> [field:type_field]...
python ninja_scaffold.py --generate-from-model <app_name> <model_name>
Examples:
python ninja_scaffold.py crm Person name:charfield email:charfield age:integerfield data:datefield
python ninja_scaffold.py crm Customer name:charfield email:charfield age:integerfield data:datefield
python ninja_scaffold.py crm Provider name:charfield email:charfield person:foreignkey
python ninja_scaffold.py product Product title:charfield price:decimalfield
python ninja_scaffold.py --generate-from-model crm Person
python ninja_scaffold.py --generate-from-model crm Customer
"""
import sys
import re
from pathlib import Path
FIELD_TYPE_MAPPING = {
'charfield': ('CharField', 'max_length=100'),
'textfield': ('TextField', 'null=True, blank=True'),
'integerfield': ('IntegerField', 'null=True, blank=True'),
'booleanfield': ('BooleanField', 'default=False'),
'decimalfield': ('DecimalField', 'decimal_places=2, max_digits=7'),
'datefield': ('DateField', 'null=True, blank=True'),
'datetimefield': ('DateTimeField', 'auto_now_add=True'),
'emailfield': ('EmailField', 'max_length=254'),
'urlfield': ('URLField', 'max_length=200, null=True, blank=True'),
'slugfield': ('SlugField', 'max_length=50'),
'uuidfield': ('UUIDField', 'default=uuid.uuid4, editable=False'),
'filefield': ('FileField', 'upload_to="uploads/", null=True, blank=True'),
'imagefield': ('ImageField', 'upload_to="images/", null=True, blank=True'),
# ForeignKey and other relation fields are handled separately
'jsonfield': ('JSONField', 'default=dict, null=True, blank=True'),
}
def validate_app_name(name):
"""Validate the app name follows Django conventions."""
if not re.match(r'^[a-z][a-z0-9_]*$', name):
print(f"Error: App name '{name}' must start with a lowercase letter and contain only lowercase letters, numbers, and underscores.")
sys.exit(1)
return name
def validate_model_name(name):
"""Validate the model name follows Django conventions."""
if not re.match(r'^[A-Z][a-zA-Z0-9]*$', name):
print(f"Error: Model name '{name}' must start with an uppercase letter and be in CamelCase.")
sys.exit(1)
return name
def validate_field_name(name):
"""Validate the field name follows Django conventions."""
if not re.match(r'^[a-z][a-z0-9_]*$', name):
print(f"Error: Field name '{name}' must start with a lowercase letter and contain only lowercase letters, numbers, and underscores.")
sys.exit(1)
return name
def get_relation_model_name(field_name):
"""Convert a field name to the appropriate related model name.
E.g., 'person' -> 'Person', 'user_profile' -> 'UserProfile'
"""
# Convert to CamelCase
parts = field_name.split('_')
return ''.join(part.capitalize() for part in parts)
def generate_model_class(model_name, fields):
"""Generate a model class for models.py file."""
field_imports = set()
for field_type in [f_type.lower() for _, f_type in fields]:
if field_type == 'uuidfield':
field_imports.add('import uuid')
field_lines = []
for field_name, field_type in fields:
field_type = field_type.lower()
# Special handling for relation fields
if field_type == 'foreignkey':
# Derive related model name from field name
related_model = get_relation_model_name(field_name)
field_lines.append(f" {field_name} = models.ForeignKey('{related_model}', on_delete=models.CASCADE, null=True, blank=True)")
elif field_type == 'manytomanyfield':
related_model = get_relation_model_name(field_name)
field_lines.append(f" {field_name} = models.ManyToManyField('{related_model}')")
elif field_type == 'onetoone':
related_model = get_relation_model_name(field_name)
field_lines.append(f" {field_name} = models.OneToOneField('{related_model}', on_delete=models.CASCADE, null=True, blank=True)")
elif field_type in FIELD_TYPE_MAPPING:
django_type, extra_args = FIELD_TYPE_MAPPING[field_type]
args = f"{extra_args}"
field_lines.append(f" {field_name} = models.{django_type}({args})")
else:
print(f"Warning: Unknown field type '{field_type}'. Using TextField instead.")
field_lines.append(f" {field_name} = models.TextField()")
# Get first field for __str__ and ordering
first_field = fields[0][0] if fields else "id"
model_class = '\n'.join([
f"class {model_name}(models.Model):",
*field_lines,
"",
" class Meta:",
f" ordering = ('{first_field}',)",
f" verbose_name = '{model_name.lower()}'",
f" verbose_name_plural = '{model_name.lower()}s'",
"",
" def __str__(self):",
f" return f'{{self.{first_field}}}'",
""
])
return model_class, field_imports
def generate_schema_class(model_name, fields):
"""Generate schema classes for schemas.py file."""
field_names = [field_name for field_name, _ in fields]
# Create output schema with ID
output_fields = ['id'] + field_names
output_fields_tuple = repr(tuple(output_fields))
# Create input schema without ID
input_fields_tuple = repr(tuple(field_names))
schema_class = '\n'.join([
f"class {model_name}Schema(ModelSchema):",
" class Meta:",
f" model = {model_name}",
f" fields = {output_fields_tuple}",
"",
f"class {model_name}InputSchema(ModelSchema):",
" class Meta:",
f" model = {model_name}",
f" fields = {input_fields_tuple}",
""
])
return schema_class
def generate_router_content(model_name, fields):
"""Generate router content for api.py file."""
model_name_lower = model_name.lower()
models_plural = f"{model_name_lower}s"
router_content = '\n'.join([
f"# {model_name} routes",
f"@router.get('/{models_plural}', response=list[{model_name}Schema], tags=['{models_plural}'])",
f"def list_{models_plural}(request):",
f" return {model_name}.objects.all()",
"",
f"@router.get('/{models_plural}/{{pk}}', response={model_name}Schema, tags=['{models_plural}'])",
f"def get_{model_name_lower}(request, pk: int):",
f" return get_object_or_404({model_name}, pk=pk)",
"",
f"@router.post('/{models_plural}', response={{HTTPStatus.CREATED: {model_name}Schema}}, tags=['{models_plural}'])",
f"def create_{model_name_lower}(request, payload: {model_name}InputSchema):",
f" return {model_name}.objects.create(**payload.dict())",
"",
f"@router.patch('/{models_plural}/{{pk}}', response={model_name}Schema, tags=['{models_plural}'])",
f"def update_{model_name_lower}(request, pk: int, payload: {model_name}InputSchema):",
f" instance = get_object_or_404({model_name}, pk=pk)",
" data = payload.dict(exclude_unset=True)",
"",
" for attr, value in data.items():",
" setattr(instance, attr, value)",
"",
" instance.save()",
" return instance",
"",
f"@router.delete('/{models_plural}/{{pk}}', tags=['{models_plural}'])",
f"def delete_{model_name_lower}(request, pk: int):",
f" instance = get_object_or_404({model_name}, pk=pk)",
" instance.delete()",
" return {'success': True}",
""
])
return router_content
def generate_admin_class(model_name, fields):
"""Generate admin class for admin.py file."""
field_names = [field_name for field_name, _ in fields]
# Get first field for search
first_field = fields[0][0] if fields else "id"
admin_class = '\n'.join([
f"@admin.register({model_name})",
f"class {model_name}Admin(admin.ModelAdmin):",
f" list_display = {repr(field_names)}",
f" search_fields = ('{first_field}',)",
" list_filter = ('created_at',)" if 'created_at' in field_names else "",
""
])
# Remove empty line if list_filter isn't included
admin_class = admin_class.replace("\n\n\n", "\n\n")
return admin_class
def update_models_py(app_dir, model_name, fields):
"""Update models.py file with a new model."""
models_file = app_dir / "models.py"
model_class, field_imports = generate_model_class(model_name, fields)
if models_file.exists():
# File exists, check if the model is already defined
content = models_file.read_text()
if f"class {model_name}(models.Model):" in content:
print(f"Model {model_name} already exists in {models_file}")
return False
# Add imports if needed
for import_stmt in field_imports:
if import_stmt not in content:
content = content.replace("from django.db import models", f"from django.db import models\n{import_stmt}")
# Add the new model class
content += "\n\n" + model_class
models_file.write_text(content)
else:
# Create new file
content = "from django.db import models\n"
for import_stmt in field_imports:
content += f"{import_stmt}\n"
content += "\n\n" + model_class
models_file.write_text(content)
return True
def update_schemas_py(app_dir, model_name, fields):
"""Update schemas.py file with new schema classes."""
schemas_file = app_dir / "schemas.py"
schema_class = generate_schema_class(model_name, fields)
if schemas_file.exists():
# File exists, check if the schema is already defined
content = schemas_file.read_text()
if f"class {model_name}Schema" in content:
print(f"Schema for {model_name} already exists in {schemas_file}")
return False
# Add the model to imports if not already there
if f"from .models import {model_name}" not in content:
if "from .models import " in content:
# Update the existing import
content = re.sub(r'from .models import (.+)', f'from .models import \\1, {model_name}', content)
else:
# Add a new import
content += f"\nfrom .models import {model_name}\n"
# Add the new schema classes
content += "\n" + schema_class
schemas_file.write_text(content)
else:
# Create new file
content = '\n'.join([
"from ninja import ModelSchema",
f"from .models import {model_name}",
"",
"",
schema_class
])
schemas_file.write_text(content)
return True
def update_api_py(app_dir, model_name, fields):
"""Update api.py file with new router content."""
api_file = app_dir / "api.py"
router_content = generate_router_content(model_name, fields)
if api_file.exists():
# File exists, check if the routes are already defined
content = api_file.read_text()
if f"def list_{model_name.lower()}s" in content:
print(f"Routes for {model_name} already exist in {api_file}")
return False
# Add the model and schemas to imports if not already there
model_import = f"from .models import {model_name}"
schema_import = f"from .schemas import {model_name}Schema, {model_name}InputSchema"
if model_name not in re.findall(r'from .models import (.+)', content):
if "from .models import " in content:
# Update the existing import
content = re.sub(r'from .models import (.+)', f'from .models import \\1, {model_name}', content)
else:
# Add a new import
content = content.replace("from ninja import Router", f"from ninja import Router\n{model_import}")
if f"{model_name}Schema" not in content:
if "from .schemas import " in content:
# Update the existing import
content = re.sub(r'from .schemas import (.+)',
f'from .schemas import \\1, {model_name}Schema, {model_name}InputSchema', content)
else:
# Add a new import
content = content.replace("from ninja import Router", f"from ninja import Router\n{schema_import}")
# Add the new router content
content += "\n" + router_content
api_file.write_text(content)
else:
# Create new file
content = '\n'.join([
"from http import HTTPStatus",
"from django.shortcuts import get_object_or_404",
"from ninja import Router",
f"from .models import {model_name}",
f"from .schemas import {model_name}Schema, {model_name}InputSchema",
"",
"",
"router = Router()",
"",
router_content
])
api_file.write_text(content)
return True
def update_admin_py(app_dir, model_name, fields):
"""Update admin.py file with new admin class."""
admin_file = app_dir / "admin.py"
admin_class = generate_admin_class(model_name, fields)
if admin_file.exists():
# File exists, check if the admin class is already defined
content = admin_file.read_text()
if f"class {model_name}Admin" in content:
print(f"Admin class for {model_name} already exists in {admin_file}")
return False
# Add the model to imports if not already there
if f"from .models import {model_name}" not in content:
if "from .models import " in content:
# Update the existing import
content = re.sub(r'from .models import (.+)', f'from .models import \\1, {model_name}', content)
else:
# Add a new import
content += f"\nfrom .models import {model_name}\n"
# Add the new admin class
content += "\n" + admin_class
admin_file.write_text(content)
else:
# Create new file
content = '\n'.join([
"from django.contrib import admin",
f"from .models import {model_name}",
"",
"",
admin_class
])
admin_file.write_text(content)
return True
def extract_model_fields(app_name, model_name):
"""Extract fields from an existing model by parsing the models.py file."""
models_file = Path(app_name) / "models.py"
if not models_file.exists():
print(f"Error: File {models_file} not found.")
sys.exit(1)
try:
content = models_file.read_text()
# Find the model class definition
pattern = rf"class\s+{model_name}\s*\([^)]*\):(.+?)(?:class\s+\w+|\Z)"
match = re.search(pattern, content, re.DOTALL)
if not match:
print(f"Error: Model '{model_name}' not found in {models_file}")
sys.exit(1)
model_content = match.group(1)
# Extract field definitions
field_pattern = r"^\s*(\w+)\s*=\s*models\.(\w+)\s*\("
field_matches = re.finditer(field_pattern, model_content, re.MULTILINE)
fields = []
for match in field_matches:
field_name = match.group(1)
field_type = match.group(2).lower()
if field_name not in ['id', 'objects']:
fields.append((field_name, field_type))
if not fields:
print(f"Warning: No fields found in model '{model_name}'.")
else:
print(f"Extracted {len(fields)} fields from {models_file}:")
for name, field_type in fields:
print(f" - {name}: {field_type}")
return fields
except Exception as e:
print(f"Error: Failed to parse model file: {str(e)}")
import traceback
traceback.print_exc()
sys.exit(1)
def generate_from_model(app_name, model_name):
"""Generate schemas, API, and admin for an existing model."""
print(f"Generating from existing model '{model_name}' in app '{app_name}'...")
# Extract fields from the model
fields = extract_model_fields(app_name, model_name)
if not fields:
print(f"Warning: No fields found for model '{model_name}' in app '{app_name}'.")
return
app_dir = Path(app_name)
# Update schemas.py
schemas_updated = update_schemas_py(app_dir, model_name, fields)
# Update api.py
api_updated = update_api_py(app_dir, model_name, fields)
# Update admin.py
admin_updated = update_admin_py(app_dir, model_name, fields)
print(f"Successfully generated files for model '{model_name}' in app '{app_name}':")
if schemas_updated:
print(f" - {app_name}/schemas.py (updated)")
if api_updated:
print(f" - {app_name}/api.py (updated)")
if admin_updated:
print(f" - {app_name}/admin.py (updated)")
def create_or_update_app_structure(app_name, model_name, fields):
"""Create or update the app directory structure and files."""
# Create app directory
app_dir = Path(app_name)
app_dir.mkdir(exist_ok=True)
# Create __init__.py
init_file = app_dir / "__init__.py"
init_file.touch()
# Create or update models.py
models_updated = update_models_py(app_dir, model_name, fields)
# Create or update schemas.py
schemas_updated = update_schemas_py(app_dir, model_name, fields)
# Create or update api.py
api_updated = update_api_py(app_dir, model_name, fields)
# Create or update admin.py
admin_updated = update_admin_py(app_dir, model_name, fields)
# Create apps.py if it doesn't exist
apps_file = app_dir / "apps.py"
if not apps_file.exists():
class_name = ''.join(word.capitalize() for word in app_name.split('_'))
apps_content = '\n'.join([
"from django.apps import AppConfig",
"",
"",
f"class {class_name}Config(AppConfig):",
f" default_auto_field = 'django.db.models.BigAutoField'",
f" name = '{app_name}'",
"",
" def ready(self):",
" pass # Import signals and other initialization here",
""
])
apps_file.write_text(apps_content)
print(f"Successfully {'updated' if any([models_updated, schemas_updated, api_updated, admin_updated]) else 'added'} model '{model_name}' to app '{app_name}'")
print(f"Files:")
print(f" - {app_name}/__init__.py")
print(f" - {app_name}/models.py")
print(f" - {app_name}/schemas.py")
print(f" - {app_name}/api.py")
print(f" - {app_name}/admin.py")
print(f" - {app_name}/apps.py")
def parse_fields(field_args):
"""Parse field arguments in the format field:type."""
fields = []
for arg in field_args:
parts = arg.split(':')
if len(parts) != 2:
print(f"Error: Invalid field format '{arg}'. Use 'field:type'.")
sys.exit(1)
field_name, field_type = parts
validate_field_name(field_name)
fields.append((field_name, field_type))
return fields
def main():
"""Main entry point for the CLI."""
# Check if using the --generate-from-model flag
if len(sys.argv) > 1 and sys.argv[1] == "--generate-from-model":
if len(sys.argv) < 4:
print("Error: Not enough arguments for --generate-from-model.")
print(__doc__)
sys.exit(1)
app_name = validate_app_name(sys.argv[2])
model_name = validate_model_name(sys.argv[3])
generate_from_model(app_name, model_name)
return
# Standard mode
if len(sys.argv) < 3:
print(__doc__)
sys.exit(1)
app_name = validate_app_name(sys.argv[1])
model_name = validate_model_name(sys.argv[2])
fields = parse_fields(sys.argv[3:])
# Check that we have at least one field
if not fields:
print("Error: At least one field must be provided.")
print(__doc__)
sys.exit(1)
create_or_update_app_structure(app_name, model_name, fields)
if __name__ == "__main__":
main()
from django.contrib import admin
from django.urls import path
from .api import api
urlpatterns = [
path('admin/', admin.site.urls),
]
api_urlpatterns = [
path('api/v1/', api.urls),
]
urlpatterns += api_urlpatterns
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment