├── {{project_name}} ├── __init__.py ├── core │ ├── management │ │ ├── .gitkeep │ │ ├── __init__.py.jinja │ │ └── commands │ │ │ ├── __init__.py.jinja │ │ │ └── ninja_scaffold.py.jinja │ ├── apps.py.jinja │ ├── __init__.py.jinja │ ├── schemas.py.jinja │ └── api.py.jinja ├── api.py.jinja ├── urls.py.jinja ├── asgi.py.jinja ├── wsgi.py.jinja └── settings.py.jinja ├── img ├── copier.gif └── swagger.png ├── requirements.txt.jinja ├── copier.yml ├── env.sample ├── manage.py.jinja ├── .gitignore ├── .gitignore.jinja ├── README.md.jinja └── README.md /{{project_name}}/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /{{project_name}}/core/management/.gitkeep: -------------------------------------------------------------------------------- 1 | # Marker file 2 | -------------------------------------------------------------------------------- /{{project_name}}/core/management/__init__.py.jinja: -------------------------------------------------------------------------------- 1 | # Management commands 2 | -------------------------------------------------------------------------------- /{{project_name}}/core/management/commands/__init__.py.jinja: -------------------------------------------------------------------------------- 1 | # Commands 2 | -------------------------------------------------------------------------------- /img/copier.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rg3915/copier-django-template/main/img/copier.gif -------------------------------------------------------------------------------- /img/swagger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rg3915/copier-django-template/main/img/swagger.png -------------------------------------------------------------------------------- /requirements.txt.jinja: -------------------------------------------------------------------------------- 1 | django-extensions==4.1 2 | django-ninja>=1.4 3 | python-decouple==3.8 4 | -------------------------------------------------------------------------------- /{{project_name}}/core/apps.py.jinja: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CoreConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = '{{project_name}}.core' 7 | -------------------------------------------------------------------------------- /{{project_name}}/core/__init__.py.jinja: -------------------------------------------------------------------------------- 1 | def update_instance(instance, payload): 2 | data = payload.dict() 3 | 4 | for attr, value in data.items(): 5 | setattr(instance, attr, value) 6 | 7 | instance.save() 8 | -------------------------------------------------------------------------------- /{{project_name}}/api.py.jinja: -------------------------------------------------------------------------------- 1 | from ninja import NinjaAPI, Redoc # noqa F401 2 | 3 | 4 | api = NinjaAPI() 5 | # api = NinjaAPI(docs=Redoc()) 6 | 7 | api.add_router('', '{{project_name}}.core.api.router') 8 | 9 | # Adiciona mais apps aqui 10 | # api.add_router('', '{{project_name}}.person.api.router') 11 | -------------------------------------------------------------------------------- /{{project_name}}/urls.py.jinja: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import path 3 | 4 | from .api import api 5 | 6 | urlpatterns = [ 7 | path('admin/', admin.site.urls), 8 | ] 9 | 10 | api_urlpatterns = [ 11 | path('api/v1/', api.urls), 12 | ] 13 | 14 | urlpatterns += api_urlpatterns 15 | -------------------------------------------------------------------------------- /copier.yml: -------------------------------------------------------------------------------- 1 | project_name: 2 | type: str 3 | help: What is your project name? 4 | default: apps 5 | 6 | _exclude: 7 | - "copier.yaml" 8 | - "copier.yml" 9 | - "~*" 10 | - "*.py[co]" 11 | - "__pycache__" 12 | - ".git" 13 | - ".DS_Store" 14 | - ".svn" 15 | - ".venv" 16 | - "README.md" 17 | -------------------------------------------------------------------------------- /{{project_name}}/asgi.py.jinja: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for {{project_name}} project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', '{{project_name}}.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /{{project_name}}/wsgi.py.jinja: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for {{project_name}} project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', '{{project_name}}.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /env.sample: -------------------------------------------------------------------------------- 1 | DEBUG=True 2 | SECRET_KEY=insecure-secretkey-UwquZ!rQEM(^hdyIlCYK4s$O60TG1avne%Bjmf0R3PVFoi7Ncb 3 | ALLOWED_HOSTS=127.0.0.1,.localhost,0.0.0.0 4 | 5 | #DATABASE_URL=postgres://USER:PASSWORD@HOST:PORT/NAME 6 | #POSTGRES_DB= 7 | #POSTGRES_USER= 8 | #POSTGRES_PASSWORD=oX9P_G03A@c4FBSp1Zak 9 | #DB_HOST=localhost 10 | 11 | #DEFAULT_FROM_EMAIL= 12 | #EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend 13 | #EMAIL_HOST=localhost 14 | #EMAIL_PORT= 15 | #EMAIL_HOST_USER= 16 | #EMAIL_HOST_PASSWORD= 17 | #EMAIL_USE_TLS=True -------------------------------------------------------------------------------- /{{project_name}}/core/schemas.py.jinja: -------------------------------------------------------------------------------- 1 | from ninja import Field, ModelSchema, Schema 2 | 3 | from django.contrib.auth.models import User 4 | 5 | 6 | class StatusSchema(Schema): 7 | status: str 8 | 9 | 10 | class UserSchema(ModelSchema): 11 | full_name: str = Field(None, alias='get_full_name') 12 | username: str = Field(None) 13 | 14 | class Meta: 15 | model = User 16 | exclude = ['password', 'last_login', 'date_joined', 'user_permissions', 'groups'] 17 | 18 | 19 | class UserInputSchema(ModelSchema): 20 | class Meta: 21 | model = User 22 | fields = ['username', 'first_name', 'last_name', 'email'] 23 | -------------------------------------------------------------------------------- /manage.py.jinja: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', '{{project_name}}.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /{{project_name}}/core/api.py.jinja: -------------------------------------------------------------------------------- 1 | from http import HTTPStatus 2 | from django.shortcuts import get_object_or_404 3 | from ninja import Router 4 | from django.contrib.auth.models import User 5 | 6 | from .schemas import StatusSchema, UserInputSchema, UserSchema 7 | 8 | from . import update_instance 9 | 10 | 11 | router = Router(tags=['Core']) 12 | 13 | 14 | @router.get( 15 | 'healthcheck', 16 | response=StatusSchema, 17 | tags=['Health Check'], 18 | summary='Health Check', 19 | description='Verificação de status que permite monitorar a saúde da API.' 20 | ) 21 | def healthcheck(request): 22 | return HTTPStatus.OK, {'status': 'ok'} 23 | 24 | 25 | @router.get( 26 | 'users', 27 | response=list[UserSchema], 28 | summary='Listar Usuários', 29 | description='Retorna uma lista com todos os usuários cadastrados no sistema.' 30 | ) 31 | def list_users(request): 32 | return User.objects.all() 33 | 34 | 35 | @router.get( 36 | 'users/{pk}', 37 | response=UserSchema, 38 | summary='Obter Usuário', 39 | description='Retorna os detalhes de um usuário específico a partir do seu ID.' 40 | ) 41 | def get_user(request, pk: int): 42 | return get_object_or_404(User, pk=pk) 43 | 44 | 45 | @router.post( 46 | 'users', 47 | response={HTTPStatus.CREATED: UserSchema}, 48 | summary='Criar Usuário', 49 | description='Cria um novo usuário no sistema com base nos dados fornecidos.' 50 | ) 51 | def create_user(request, payload: UserInputSchema): 52 | return User.objects.create(**payload.dict()) 53 | 54 | 55 | @router.patch( 56 | 'users/{pk}', 57 | response=UserSchema, 58 | summary='Atualizar Usuário', 59 | description='Atualiza parcialmente os dados de um usuário específico a partir do seu ID.' 60 | ) 61 | def update_user(request, pk: int, payload: UserInputSchema): 62 | instance = get_object_or_404(User, pk=pk) 63 | update_instance(instance, payload) 64 | return instance 65 | 66 | 67 | @router.delete( 68 | 'users/{pk}', 69 | summary='Excluir Usuário', 70 | description='Remove permanentemente um usuário do sistema a partir do seu ID.' 71 | ) 72 | def delete_user(request, pk: int): 73 | instance = get_object_or_404(User, pk=pk) 74 | instance.delete() 75 | return {'success': True} 76 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | .DS_Store 132 | 133 | media/ 134 | staticfiles/ 135 | .idea 136 | .ipynb_checkpoints/ 137 | .vscode 138 | *.cast 139 | -------------------------------------------------------------------------------- /.gitignore.jinja: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | .DS_Store 132 | 133 | media/ 134 | staticfiles/ 135 | .idea 136 | .ipynb_checkpoints/ 137 | .vscode 138 | *.cast 139 | -------------------------------------------------------------------------------- /{{project_name}}/settings.py.jinja: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from decouple import config, Csv 4 | 5 | BASE_DIR = Path(__file__).resolve().parent.parent 6 | 7 | # SECURITY WARNING: keep the secret key used in production secret! 8 | SECRET_KEY = config('SECRET_KEY') 9 | 10 | # SECURITY WARNING: don't run with debug turned on in production! 11 | DEBUG = config('DEBUG', default=False, cast=bool) 12 | 13 | ALLOWED_HOSTS = config('ALLOWED_HOSTS', default=[], cast=Csv()) 14 | 15 | INSTALLED_APPS = [ 16 | 'django.contrib.admin', 17 | 'django.contrib.auth', 18 | 'django.contrib.contenttypes', 19 | 'django.contrib.sessions', 20 | 'django.contrib.messages', 21 | 'django.contrib.staticfiles', 22 | # others apps 23 | 'django_extensions', 24 | # my apps 25 | '{{project_name}}.core', 26 | ] 27 | 28 | MIDDLEWARE = [ 29 | 'django.middleware.security.SecurityMiddleware', 30 | 'django.contrib.sessions.middleware.SessionMiddleware', 31 | 'django.middleware.common.CommonMiddleware', 32 | 'django.middleware.csrf.CsrfViewMiddleware', 33 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 34 | 'django.contrib.messages.middleware.MessageMiddleware', 35 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 36 | ] 37 | 38 | ROOT_URLCONF = '{{project_name}}.urls' 39 | 40 | TEMPLATES = [ 41 | { 42 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 43 | 'DIRS': [], 44 | 'APP_DIRS': True, 45 | 'OPTIONS': { 46 | 'context_processors': [ 47 | 'django.template.context_processors.request', 48 | 'django.contrib.auth.context_processors.auth', 49 | 'django.contrib.messages.context_processors.messages', 50 | ], 51 | }, 52 | }, 53 | ] 54 | 55 | WSGI_APPLICATION = '{{project_name}}.wsgi.application' 56 | 57 | 58 | # Database 59 | # https://docs.djangoproject.com/en/5.2/ref/settings/#databases 60 | 61 | DATABASES = { 62 | 'default': { 63 | 'ENGINE': 'django.db.backends.sqlite3', 64 | 'NAME': BASE_DIR / 'db.sqlite3', 65 | } 66 | } 67 | 68 | 69 | # Password validation 70 | # https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators 71 | 72 | AUTH_PASSWORD_VALIDATORS = [ 73 | { 74 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 75 | }, 76 | { 77 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 78 | }, 79 | { 80 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 81 | }, 82 | { 83 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 84 | }, 85 | ] 86 | 87 | 88 | # Internationalization 89 | # https://docs.djangoproject.com/en/5.2/topics/i18n/ 90 | 91 | LANGUAGE_CODE = 'pt-br' 92 | 93 | TIME_ZONE = 'America/Sao_Paulo' 94 | 95 | USE_I18N = True 96 | 97 | USE_TZ = True 98 | 99 | 100 | # Static files (CSS, JavaScript, Images) 101 | # https://docs.djangoproject.com/en/5.2/howto/static-files/ 102 | 103 | STATIC_URL = 'static/' 104 | STATIC_ROOT = BASE_DIR.joinpath('staticfiles') 105 | 106 | # Default primary key field type 107 | # https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field 108 | 109 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 110 | -------------------------------------------------------------------------------- /README.md.jinja: -------------------------------------------------------------------------------- 1 | A Django project using Django Ninja for API development. 2 | 3 | Version: **1.1** 4 | 5 | ## This project made with: 6 | 7 | * [Python 3.13.3](https://www.python.org/) 8 | * [Django 5.2](https://www.djangoproject.com/) 9 | * [Django-Ninja 1.4.0](https://django-ninja.dev/) 10 | 11 | 12 | ## Setup 13 | 14 | 1. Create a virtual environment: 15 | ```bash 16 | python -m venv .venv 17 | source .venv/bin/activate # On Windows: .venv\Scripts\activate 18 | ``` 19 | 20 | 2. Install requirements: 21 | ```bash 22 | pip install -r requirements.txt 23 | ``` 24 | 25 | 3. Create .env: 26 | ```bash 27 | cp env.sample .env 28 | ``` 29 | 30 | 4. Run migrations: 31 | ```bash 32 | python manage.py migrate 33 | ``` 34 | 35 | 5. Create super user: 36 | ```bash 37 | python manage.py createsuperuser 38 | ``` 39 | 40 | 6. Start the development server: 41 | ```bash 42 | python manage.py runserver 43 | ``` 44 | 45 | ## API Documentation 46 | 47 | API documentation is available at `/api/docs` 48 | 49 | ## Ninja Scaffold Command 50 | 51 | This template includes a powerful Django management command to quickly scaffold apps with models and APIs. 52 | 53 | **Create a new model with fields:** 54 | 55 | ```bash 56 | python manage.py ninja_scaffold crm Person name:charfield email:emailfield age:integerfield 57 | ``` 58 | 59 | **Create model with relationships:** 60 | 61 | ```bash 62 | python manage.py ninja_scaffold crm Provider name:charfield email:charfield person:foreignkey 63 | ``` 64 | 65 | **Generate schemas, API and admin from existing model:** 66 | 67 | ```bash 68 | python manage.py ninja_scaffold --generate-from-model crm Person 69 | ``` 70 | 71 | **Supported field types:** 72 | - Basic: `charfield`, `textfield`, `integerfield`, `booleanfield` 73 | - Numbers/Dates: `decimalfield`, `datefield`, `datetimefield` 74 | - Special: `emailfield`, `urlfield`, `slugfield`, `uuidfield` 75 | - Files: `filefield`, `imagefield` 76 | - Advanced: `jsonfield`, `foreignkey`, `manytomanyfield`, `onetoone` 77 | 78 | **The command automatically generates:** 79 | - `models.py` - Django model with specified fields 80 | - `schemas.py` - Django Ninja schemas (input/output) 81 | - `api.py` - Complete CRUD routes (GET, POST, PATCH, DELETE) 82 | - `admin.py` - Django admin interface 83 | - `apps.py` - App configuration with correct project path 84 | - **Automatically updates** `settings.py` - Adds app to INSTALLED_APPS 85 | - **Automatically updates** `api.py` - Adds router to main API 86 | 87 | **Complete workflow example:** 88 | 89 | ```bash 90 | # Enter the project folder 91 | cd {{ project_name }} 92 | 93 | # Create first app with model 94 | python ../manage.py ninja_scaffold crm Person name:charfield email:emailfield age:integerfield 95 | python manage.py makemigrations crm 96 | python manage.py migrate 97 | 98 | # Add another model to the same app 99 | python ../manage.py ninja_scaffold crm Customer name:charfield email:emailfield age:integerfield 100 | python manage.py makemigrations 101 | python manage.py migrate 102 | 103 | # Create a new app with model 104 | python ../manage.py ninja_scaffold product Product title:charfield sku:charfield 105 | python manage.py makemigrations product 106 | python manage.py migrate 107 | ``` 108 | 109 | ## pt-br 110 | 111 | Um projeto Django usando Django Ninja para desenvolvimento de APIs. 112 | 113 | ## Este projeto foi feito com: 114 | 115 | * [Python 3.13.3](https://www.python.org/) 116 | * [Django 5.2](https://www.djangoproject.com/) 117 | * [Django-Ninja 1.4.0](https://django-ninja.dev/) 118 | 119 | 120 | ## Configuração 121 | 122 | 1. Crie um ambiente virtual: 123 | ```bash 124 | python -m venv .venv 125 | source .venv/bin/activate # On Windows: .venv\Scripts\activate 126 | ``` 127 | 128 | 2. Instale as dependências: 129 | ```bash 130 | pip install -r requirements.txt 131 | ``` 132 | 133 | 3. Crie o arquivo .env: 134 | ```bash 135 | cp env.sample .env 136 | ``` 137 | 138 | 4. Rode as migrações: 139 | ```bash 140 | python manage.py migrate 141 | ``` 142 | 143 | 5. Crie um super usuário: 144 | ```bash 145 | python manage.py createsuperuser 146 | ``` 147 | 148 | 6. Inicie o servidor de desenvolvimento: 149 | ```bash 150 | python manage.py runserver 151 | ``` 152 | 153 | ## Documentação da API 154 | 155 | A documentação da API está disponível em `/api/docs` 156 | 157 | ## Comando Ninja Scaffold 158 | 159 | Este template inclui um poderoso comando de gerenciamento Django para criar rapidamente apps com models e APIs. 160 | 161 | **Criar um novo model com campos:** 162 | 163 | ```bash 164 | python manage.py ninja_scaffold crm Person name:charfield email:emailfield age:integerfield 165 | ``` 166 | 167 | **Criar model com relacionamentos:** 168 | 169 | ```bash 170 | python manage.py ninja_scaffold crm Provider name:charfield email:charfield person:foreignkey 171 | ``` 172 | 173 | **Gerar schemas, API e admin de um model existente:** 174 | 175 | ```bash 176 | python manage.py ninja_scaffold --generate-from-model crm Person 177 | ``` 178 | 179 | **Tipos de campos suportados:** 180 | - Básicos: `charfield`, `textfield`, `integerfield`, `booleanfield` 181 | - Números/Datas: `decimalfield`, `datefield`, `datetimefield` 182 | - Especiais: `emailfield`, `urlfield`, `slugfield`, `uuidfield` 183 | - Arquivos: `filefield`, `imagefield` 184 | - Avançados: `jsonfield`, `foreignkey`, `manytomanyfield`, `onetoone` 185 | 186 | **O comando gera automaticamente:** 187 | - `models.py` - Model Django com os campos especificados 188 | - `schemas.py` - Schemas do Django Ninja (input/output) 189 | - `api.py` - Rotas CRUD completas (GET, POST, PATCH, DELETE) 190 | - `admin.py` - Interface de administração Django 191 | - `apps.py` - Configuração da app com caminho correto do projeto 192 | - **Atualiza automaticamente** `settings.py` - Adiciona app em INSTALLED_APPS 193 | - **Atualiza automaticamente** `api.py` - Adiciona router na API principal 194 | 195 | **Exemplo de workflow completo:** 196 | 197 | ```bash 198 | # Entrar na pasta do projeto 199 | cd {{ project_name }} 200 | 201 | # Criar primeira app com model 202 | python ../manage.py ninja_scaffold crm Person name:charfield email:emailfield age:integerfield 203 | python manage.py makemigrations crm 204 | python manage.py migrate 205 | 206 | # Adicionar outro model na mesma app 207 | python ../manage.py ninja_scaffold crm Customer name:charfield email:emailfield age:integerfield 208 | python manage.py makemigrations 209 | python manage.py migrate 210 | 211 | # Criar uma nova app com model 212 | python ../manage.py ninja_scaffold product Product title:charfield sku:charfield 213 | python manage.py makemigrations product 214 | python manage.py migrate 215 | ``` 216 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # copier-django-template 2 | 3 | Version: **1.1** 4 | 5 | **English 🇺🇸 | Português 🇧🇷** 6 | 7 | ## 🇺🇸 About 8 | 9 | This project is a reusable [Copier](https://copier.readthedocs.io/) template for bootstrapping Django applications quickly and consistently. It includes a pre-configured Django project with: 10 | 11 | - Django 5.2+ 12 | - Django Ninja 1.4+ (API) 13 | - Environment variables using `python-decouple` 14 | 15 | Use this template to save time setting up new Django projects, especially when working with APIs using Django Ninja. 16 | 17 | --- 18 | 19 | ## 🇺🇸 Getting Started 20 | 21 | ### 1. Create your project folder and copier folder 22 | 23 | ```bash 24 | mkdir -p ~/projects/django_ninja_example 25 | mkdir -p ~/projects/copier_tmp 26 | ```` 27 | 28 | ### 2. Generate your project using Copier 29 | 30 | ```bash 31 | cd ~/projects/copier_tmp 32 | 33 | python -m venv .venv 34 | source .venv/bin/activate 35 | 36 | pip install copier 37 | 38 | # copier copy --vcs-ref=main https://github.com/rg3915/copier-django-template.git ~/projects/django_ninja_example 39 | 40 | git clone https://github.com/rg3915/copier-django-template.git /tmp/copier-django-template 41 | 42 | cd /tmp/copier-django-template 43 | 44 | copier copy --vcs-ref=main . ~/projects/django_ninja_example 45 | ``` 46 | 47 | > **Important:** Use `--vcs-ref=main` to get the latest version from the main branch. Without this flag, Copier tries to use Git tags, which may not include recent updates like the `ninja_scaffold` command. 48 | 49 | ### Result 50 | 51 | ``` 52 | . 53 | ├── apps 54 | │   ├── api.py 55 | │   ├── asgi.py 56 | │   ├── core 57 | │   │   ├── api.py 58 | │   │   ├── apps.py 59 | │   │   ├── __init__.py 60 | │   │   └── schemas.py 61 | │   ├── __init__.py 62 | │   ├── settings.py 63 | │   ├── urls.py 64 | │   └── wsgi.py 65 | ├── env.sample 66 | ├── manage.py 67 | ├── README.md 68 | └── requirements.txt 69 | ``` 70 | 71 | ### 3. How to run the new project 72 | 73 | ```bash 74 | deactivate # deactivate copier venv 75 | 76 | cd ~/projects/django_ninja_example 77 | 78 | python -m venv .venv 79 | source .venv/bin/activate 80 | 81 | pip install -r requirements.txt 82 | 83 | cp env.sample .env 84 | ``` 85 | 86 | ### 4. Run migrations and create a superuser 87 | 88 | ```bash 89 | python manage.py migrate 90 | python manage.py createsuperuser --username="admin" --email="" 91 | ``` 92 | 93 | ### 5. Start the development server 94 | 95 | ```bash 96 | python manage.py runserver 97 | ``` 98 | 99 | ### Docs 100 | 101 | Enter in 102 | 103 | http://localhost:8000/api/v1/docs 104 | 105 | ![](img/swagger.png) 106 | 107 | ### Ninja Scaffold Command 108 | 109 | This template includes a powerful Django management command to quickly scaffold apps with models and APIs. 110 | 111 | **Create a new model with fields:** 112 | 113 | ```bash 114 | python manage.py ninja_scaffold crm Person name:charfield email:emailfield age:integerfield 115 | ``` 116 | 117 | **Create model with relationships:** 118 | 119 | ```bash 120 | python manage.py ninja_scaffold crm Provider name:charfield email:charfield person:foreignkey 121 | ``` 122 | 123 | **Generate schemas, API and admin from existing model:** 124 | 125 | ```bash 126 | python manage.py ninja_scaffold --generate-from-model crm Person 127 | ``` 128 | 129 | **Supported field types:** 130 | - Basic: `charfield`, `textfield`, `integerfield`, `booleanfield` 131 | - Numbers/Dates: `decimalfield`, `datefield`, `datetimefield` 132 | - Special: `emailfield`, `urlfield`, `slugfield`, `uuidfield` 133 | - Files: `filefield`, `imagefield` 134 | - Advanced: `jsonfield`, `foreignkey`, `manytomanyfield`, `onetoone` 135 | 136 | **The command automatically generates:** 137 | - `models.py` - Django model with specified fields 138 | - `schemas.py` - Django Ninja schemas (input/output) 139 | - `api.py` - Complete CRUD routes (GET, POST, PATCH, DELETE) 140 | - `admin.py` - Django admin interface 141 | - `apps.py` - App configuration with correct project path 142 | - **Automatically updates** `settings.py` - Adds app to INSTALLED_APPS 143 | - **Automatically updates** `api.py` - Adds router to main API 144 | 145 | **Complete workflow example:** 146 | 147 | ```bash 148 | # Enter the project folder 149 | cd apps 150 | 151 | # Create first app with model 152 | python ../manage.py ninja_scaffold crm Person name:charfield email:emailfield age:integerfield 153 | python manage.py makemigrations crm 154 | python manage.py migrate 155 | 156 | # Add another model to the same app 157 | python ../manage.py ninja_scaffold crm Customer name:charfield email:emailfield age:integerfield 158 | python manage.py makemigrations 159 | python manage.py migrate 160 | 161 | # Create a new app with model 162 | python ../manage.py ninja_scaffold product Product title:charfield sku:charfield 163 | python manage.py makemigrations product 164 | python manage.py migrate 165 | ``` 166 | 167 | ### Plus 168 | 169 | If you want to run everything on Linux with a script, type 170 | 171 | ```bash 172 | wget https://gist.githubusercontent.com/rg3915/7842b05ff93d9fd87f977d3c0b9300d3/raw/22ca5e73ff5db83a2d48fd33dbc57bc83e2c8611/script.sh 173 | 174 | source script.sh 175 | ``` 176 | 177 | ![](img/copier.gif) 178 | 179 | --- 180 | 181 | ## 🇧🇷 Sobre 182 | 183 | Este projeto é um template reutilizável do [Copier](https://copier.readthedocs.io/) para iniciar aplicações Django de forma rápida e padronizada. Ele já vem com uma estrutura pronta contendo: 184 | 185 | * Django 5.2+ 186 | * Django Ninja 1.4+ (para APIs) 187 | * Variáveis de ambiente com `python-decouple` 188 | 189 | Ideal para quem deseja agilidade na criação de projetos Django com suporte a APIs. 190 | 191 | --- 192 | 193 | ## 🇧🇷 Como começar 194 | 195 | ### 1. Crie a pasta do seu projeto e a pasta do copier 196 | 197 | ```bash 198 | mkdir -p ~/projects/django_ninja_example 199 | mkdir -p ~/projects/copier_tmp 200 | ```` 201 | 202 | ### 2. Gere seu projeto com o Copier 203 | 204 | ```bash 205 | cd ~/projects/copier_tmp 206 | 207 | python -m venv .venv 208 | source .venv/bin/activate 209 | 210 | pip install copier 211 | 212 | copier copy --vcs-ref=main https://github.com/rg3915/copier-django-template.git ~/projects/django_ninja_example 213 | ``` 214 | 215 | > **Important:** Use `--vcs-ref=main` to get the latest version from the main branch. Without this flag, Copier tries to use Git tags, which may not include recent updates like the `ninja_scaffold` command. 216 | 217 | ### Resultado 218 | 219 | ``` 220 | . 221 | ├── apps 222 | │   ├── api.py 223 | │   ├── asgi.py 224 | │   ├── core 225 | │   │   ├── api.py 226 | │   │   ├── apps.py 227 | │   │   ├── __init__.py 228 | │   │   └── schemas.py 229 | │   ├── __init__.py 230 | │   ├── settings.py 231 | │   ├── urls.py 232 | │   └── wsgi.py 233 | ├── env.sample 234 | ├── manage.py 235 | ├── README.md 236 | └── requirements.txt 237 | ``` 238 | 239 | ### 3. Como rodar o novo projeto 240 | 241 | ```bash 242 | deactivate # desative a venv do copier 243 | 244 | cd ~/projects/django_ninja_example 245 | 246 | python -m venv .venv 247 | source .venv/bin/activate 248 | 249 | pip install -r requirements.txt 250 | 251 | cp env.sample .env 252 | ``` 253 | 254 | ### 4. Rode as migrações e crie um superusuário 255 | 256 | ```bash 257 | python manage.py migrate 258 | python manage.py createsuperuser --username="admin" --email="" 259 | ``` 260 | 261 | ### 5. Inicie o servidor de desenvolvimento 262 | 263 | ```bash 264 | python manage.py runserver 265 | ``` 266 | 267 | ### Docs 268 | 269 | Entre em 270 | 271 | http://localhost:8000/api/v1/docs 272 | 273 | ![](img/swagger.png) 274 | 275 | 276 | ### Comando Ninja Scaffold 277 | 278 | Este template inclui um poderoso comando de gerenciamento Django para criar rapidamente apps com models e APIs. 279 | 280 | **Criar um novo model com campos:** 281 | 282 | ```bash 283 | python manage.py ninja_scaffold crm Person name:charfield email:emailfield age:integerfield 284 | ``` 285 | 286 | **Criar model com relacionamentos:** 287 | 288 | ```bash 289 | python manage.py ninja_scaffold crm Provider name:charfield email:charfield person:foreignkey 290 | ``` 291 | 292 | **Gerar schemas, API e admin de um model existente:** 293 | 294 | ```bash 295 | python manage.py ninja_scaffold --generate-from-model crm Person 296 | ``` 297 | 298 | **Tipos de campos suportados:** 299 | - Básicos: `charfield`, `textfield`, `integerfield`, `booleanfield` 300 | - Números/Datas: `decimalfield`, `datefield`, `datetimefield` 301 | - Especiais: `emailfield`, `urlfield`, `slugfield`, `uuidfield` 302 | - Arquivos: `filefield`, `imagefield` 303 | - Avançados: `jsonfield`, `foreignkey`, `manytomanyfield`, `onetoone` 304 | 305 | **O comando gera automaticamente:** 306 | - `models.py` - Model Django com os campos especificados 307 | - `schemas.py` - Schemas do Django Ninja (input/output) 308 | - `api.py` - Rotas CRUD completas (GET, POST, PATCH, DELETE) 309 | - `admin.py` - Interface de administração Django 310 | - `apps.py` - Configuração da app com caminho correto do projeto 311 | - **Atualiza automaticamente** `settings.py` - Adiciona app em INSTALLED_APPS 312 | - **Atualiza automaticamente** `api.py` - Adiciona router na API principal 313 | 314 | **Exemplo de workflow completo:** 315 | 316 | ```bash 317 | # Entrar na pasta do projeto 318 | cd apps 319 | 320 | # Criar primeira app com model 321 | python ../manage.py ninja_scaffold crm Person name:charfield email:emailfield age:integerfield 322 | python manage.py makemigrations crm 323 | python manage.py migrate 324 | 325 | # Adicionar outro model na mesma app 326 | python ../manage.py ninja_scaffold crm Customer name:charfield email:emailfield age:integerfield 327 | python manage.py makemigrations 328 | python manage.py migrate 329 | 330 | # Criar uma nova app com model 331 | python ../manage.py ninja_scaffold product Product title:charfield sku:charfield 332 | python manage.py makemigrations product 333 | python manage.py migrate 334 | ``` 335 | 336 | ### Plus 337 | 338 | Se quiser rodar tudo no Linux com um script, digite 339 | 340 | ```bash 341 | wget https://gist.githubusercontent.com/rg3915/7842b05ff93d9fd87f977d3c0b9300d3/raw/22ca5e73ff5db83a2d48fd33dbc57bc83e2c8611/script.sh 342 | 343 | source script.sh 344 | ``` 345 | 346 | ![](img/copier.gif) -------------------------------------------------------------------------------- /{{project_name}}/core/management/commands/ninja_scaffold.py.jinja: -------------------------------------------------------------------------------- 1 | """ 2 | ninja_scaffold: A Django management command to quickly scaffold Django apps with models and APIs. 3 | 4 | Usage: 5 | python manage.py ninja_scaffold [field:type_field]... 6 | python manage.py ninja_scaffold --generate-from-model 7 | 8 | Examples: 9 | python manage.py ninja_scaffold crm Person name:charfield email:charfield age:integerfield data:datefield 10 | python manage.py ninja_scaffold crm Provider name:charfield email:charfield person:foreignkey 11 | python manage.py ninja_scaffold --generate-from-model crm Person 12 | """ 13 | # {{project_name}} 14 | import re 15 | from pathlib import Path 16 | from django.core.management.base import BaseCommand, CommandError 17 | from django.conf import settings 18 | 19 | FIELD_TYPE_MAPPING = { 20 | 'charfield': ('CharField', 'max_length=100'), 21 | 'textfield': ('TextField', 'null=True, blank=True'), 22 | 'integerfield': ('IntegerField', 'null=True, blank=True'), 23 | 'booleanfield': ('BooleanField', 'default=False'), 24 | 'decimalfield': ('DecimalField', 'decimal_places=2, max_digits=7'), 25 | 'datefield': ('DateField', 'null=True, blank=True'), 26 | 'datetimefield': ('DateTimeField', 'auto_now_add=True'), 27 | 'emailfield': ('EmailField', 'max_length=254'), 28 | 'urlfield': ('URLField', 'max_length=200, null=True, blank=True'), 29 | 'slugfield': ('SlugField', 'max_length=50'), 30 | 'uuidfield': ('UUIDField', 'default=uuid.uuid4, editable=False'), 31 | 'filefield': ('FileField', 'upload_to="uploads/", null=True, blank=True'), 32 | 'imagefield': ('ImageField', 'upload_to="images/", null=True, blank=True'), 33 | 'jsonfield': ('JSONField', 'default=dict, null=True, blank=True'), 34 | } 35 | 36 | 37 | class Command(BaseCommand): 38 | help = 'Scaffold Django apps with models and APIs using Django Ninja' 39 | 40 | def add_arguments(self, parser): 41 | parser.add_argument('app_name', type=str, help='Name of the Django app') 42 | parser.add_argument('model_name', type=str, help='Name of the model') 43 | parser.add_argument( 44 | 'fields', 45 | nargs='*', 46 | type=str, 47 | help='Fields in format field:type (e.g., name:charfield email:emailfield)' 48 | ) 49 | parser.add_argument( 50 | '--generate-from-model', 51 | action='store_true', 52 | help='Generate schemas, API, and admin from an existing model' 53 | ) 54 | 55 | def handle(self, *args, **options): 56 | app_name = options['app_name'] 57 | model_name = options['model_name'] 58 | field_args = options['fields'] 59 | generate_from_model = options['generate_from_model'] 60 | 61 | # Validate names 62 | self.validate_app_name(app_name) 63 | self.validate_model_name(model_name) 64 | 65 | if generate_from_model: 66 | self.generate_from_existing_model(app_name, model_name) 67 | else: 68 | if not field_args: 69 | raise CommandError("Error: At least one field must be provided.") 70 | 71 | fields = self.parse_fields(field_args) 72 | self.create_or_update_app_structure(app_name, model_name, fields) 73 | 74 | def validate_app_name(self, name): 75 | """Validate the app name follows Django conventions.""" 76 | if not re.match(r'^[a-z][a-z0-9_]*$', name): 77 | raise CommandError( 78 | f"App name '{name}' must start with a lowercase letter and contain only lowercase letters, numbers, and underscores." 79 | ) 80 | 81 | def validate_model_name(self, name): 82 | """Validate the model name follows Django conventions.""" 83 | if not re.match(r'^[A-Z][a-zA-Z0-9]*$', name): 84 | raise CommandError( 85 | f"Model name '{name}' must start with an uppercase letter and be in CamelCase." 86 | ) 87 | 88 | def validate_field_name(self, name): 89 | """Validate the field name follows Django conventions.""" 90 | if not re.match(r'^[a-z][a-z0-9_]*$', name): 91 | raise CommandError( 92 | f"Field name '{name}' must start with a lowercase letter and contain only lowercase letters, numbers, and underscores." 93 | ) 94 | 95 | def get_project_name(self): 96 | """Get the project name from settings.ROOT_URLCONF.""" 97 | root_urlconf = settings.ROOT_URLCONF 98 | return root_urlconf.split('.')[0] if root_urlconf else None 99 | 100 | def get_relation_model_name(self, field_name): 101 | """Convert a field name to the appropriate related model name. 102 | E.g., 'person' -> 'Person', 'user_profile' -> 'UserProfile' 103 | """ 104 | parts = field_name.split('_') 105 | return ''.join(part.capitalize() for part in parts) 106 | 107 | def parse_fields(self, field_args): 108 | """Parse field arguments in the format field:type.""" 109 | fields = [] 110 | for arg in field_args: 111 | parts = arg.split(':') 112 | if len(parts) != 2: 113 | raise CommandError(f"Invalid field format '{arg}'. Use 'field:type'.") 114 | field_name, field_type = parts 115 | self.validate_field_name(field_name) 116 | fields.append((field_name, field_type)) 117 | return fields 118 | 119 | def generate_model_class(self, model_name, fields): 120 | """Generate a model class for models.py file.""" 121 | field_imports = set() 122 | 123 | for field_type in [f_type.lower() for _, f_type in fields]: 124 | if field_type == 'uuidfield': 125 | field_imports.add('import uuid') 126 | 127 | field_lines = [] 128 | for field_name, field_type in fields: 129 | field_type = field_type.lower() 130 | 131 | if field_type == 'foreignkey': 132 | related_model = self.get_relation_model_name(field_name) 133 | field_lines.append(f" {field_name} = models.ForeignKey('{related_model}', on_delete=models.CASCADE, null=True, blank=True)") 134 | elif field_type == 'manytomanyfield': 135 | related_model = self.get_relation_model_name(field_name) 136 | field_lines.append(f" {field_name} = models.ManyToManyField('{related_model}')") 137 | elif field_type == 'onetoone': 138 | related_model = self.get_relation_model_name(field_name) 139 | field_lines.append(f" {field_name} = models.OneToOneField('{related_model}', on_delete=models.CASCADE, null=True, blank=True)") 140 | elif field_type in FIELD_TYPE_MAPPING: 141 | django_type, extra_args = FIELD_TYPE_MAPPING[field_type] 142 | field_lines.append(f" {field_name} = models.{django_type}({extra_args})") 143 | else: 144 | self.stdout.write(self.style.WARNING(f"Warning: Unknown field type '{field_type}'. Using TextField instead.")) 145 | field_lines.append(f" {field_name} = models.TextField()") 146 | 147 | first_field = fields[0][0] if fields else "id" 148 | 149 | model_class = '\n'.join([ 150 | f"class {model_name}(models.Model):", 151 | *field_lines, 152 | "", 153 | " class Meta:", 154 | f" ordering = ('{first_field}',)", 155 | f" verbose_name = '{model_name.lower()}'", 156 | f" verbose_name_plural = '{model_name.lower()}s'", 157 | "", 158 | " def __str__(self):", 159 | f" return f'{{ '{{' }}self.{first_field}{{ '}}' }}'", 160 | "" 161 | ]) 162 | 163 | return model_class, field_imports 164 | 165 | def generate_schema_class(self, model_name, fields): 166 | """Generate schema classes for schemas.py file.""" 167 | field_names = [field_name for field_name, _ in fields] 168 | 169 | output_fields = ['id'] + field_names 170 | output_fields_tuple = repr(tuple(output_fields)) 171 | 172 | input_fields_tuple = repr(tuple(field_names)) 173 | 174 | schema_class = '\n'.join([ 175 | f"class {model_name}Schema(ModelSchema):", 176 | " class Meta:", 177 | f" model = {model_name}", 178 | f" fields = {output_fields_tuple}", 179 | "", 180 | f"class {model_name}InputSchema(ModelSchema):", 181 | " class Meta:", 182 | f" model = {model_name}", 183 | f" fields = {input_fields_tuple}", 184 | "" 185 | ]) 186 | 187 | return schema_class 188 | 189 | def generate_router_content(self, model_name, fields): 190 | """Generate router content for api.py file.""" 191 | model_name_lower = model_name.lower() 192 | models_plural = f"{model_name_lower}s" 193 | 194 | router_content = '\n'.join([ 195 | f"# {model_name} routes", 196 | f"@router.get('/{models_plural}', response=list[{model_name}Schema], tags=['{models_plural}'])", 197 | f"def list_{models_plural}(request):", 198 | f" return {model_name}.objects.all()", 199 | "", 200 | f"@router.get('/{models_plural}/{{ '{{' }}pk{{ '}}' }}', response={model_name}Schema, tags=['{models_plural}'])", 201 | f"def get_{model_name_lower}(request, pk: int):", 202 | f" return get_object_or_404({model_name}, pk=pk)", 203 | "", 204 | f"@router.post('/{models_plural}', response={{ '{{' }}HTTPStatus.CREATED: {model_name}Schema{{ '}}' }}, tags=['{models_plural}'])", 205 | f"def create_{model_name_lower}(request, payload: {model_name}InputSchema):", 206 | f" return {model_name}.objects.create(**payload.dict())", 207 | "", 208 | f"@router.patch('/{models_plural}/{{ '{{' }}pk{{ '}}' }}', response={model_name}Schema, tags=['{models_plural}'])", 209 | f"def update_{model_name_lower}(request, pk: int, payload: {model_name}InputSchema):", 210 | f" instance = get_object_or_404({model_name}, pk=pk)", 211 | " data = payload.dict(exclude_unset=True)", 212 | "", 213 | " for attr, value in data.items():", 214 | " setattr(instance, attr, value)", 215 | "", 216 | " instance.save()", 217 | " return instance", 218 | "", 219 | f"@router.delete('/{models_plural}/{{ '{{' }}pk{{ '}}' }}', tags=['{models_plural}'])", 220 | f"def delete_{model_name_lower}(request, pk: int):", 221 | f" instance = get_object_or_404({model_name}, pk=pk)", 222 | " instance.delete()", 223 | " return {'success': True}", 224 | "" 225 | ]) 226 | 227 | return router_content 228 | 229 | def generate_admin_class(self, model_name, fields): 230 | """Generate admin class for admin.py file.""" 231 | field_names = [field_name for field_name, _ in fields] 232 | first_field = fields[0][0] if fields else "id" 233 | 234 | admin_class = '\n'.join([ 235 | f"@admin.register({model_name})", 236 | f"class {model_name}Admin(admin.ModelAdmin):", 237 | f" list_display = {repr(field_names)}", 238 | f" search_fields = ('{first_field}',)", 239 | " list_filter = ('created_at',)" if 'created_at' in field_names else "", 240 | "" 241 | ]) 242 | 243 | admin_class = admin_class.replace("\n\n\n", "\n\n") 244 | 245 | return admin_class 246 | 247 | def update_models_py(self, app_dir, model_name, fields): 248 | """Update models.py file with a new model.""" 249 | models_file = app_dir / "models.py" 250 | model_class, field_imports = self.generate_model_class(model_name, fields) 251 | 252 | if models_file.exists(): 253 | content = models_file.read_text() 254 | 255 | if f"class {model_name}(models.Model):" in content: 256 | self.stdout.write(self.style.WARNING(f"Model {model_name} already exists in {models_file}")) 257 | return False 258 | 259 | for import_stmt in field_imports: 260 | if import_stmt not in content: 261 | content = content.replace("from django.db import models", f"from django.db import models\n{import_stmt}") 262 | 263 | content += "\n\n" + model_class 264 | models_file.write_text(content) 265 | else: 266 | content = "from django.db import models\n" 267 | for import_stmt in field_imports: 268 | content += f"{import_stmt}\n" 269 | content += "\n\n" + model_class 270 | models_file.write_text(content) 271 | 272 | return True 273 | 274 | def update_schemas_py(self, app_dir, model_name, fields): 275 | """Update schemas.py file with new schema classes.""" 276 | schemas_file = app_dir / "schemas.py" 277 | schema_class = self.generate_schema_class(model_name, fields) 278 | 279 | if schemas_file.exists(): 280 | content = schemas_file.read_text() 281 | 282 | if f"class {model_name}Schema" in content: 283 | self.stdout.write(self.style.WARNING(f"Schema for {model_name} already exists in {schemas_file}")) 284 | return False 285 | 286 | if f"from .models import {model_name}" not in content: 287 | if "from .models import " in content: 288 | content = re.sub(r'from .models import (.+)', f'from .models import \\1, {model_name}', content) 289 | else: 290 | content += f"\nfrom .models import {model_name}\n" 291 | 292 | content += "\n" + schema_class 293 | schemas_file.write_text(content) 294 | else: 295 | content = '\n'.join([ 296 | "from ninja import ModelSchema", 297 | f"from .models import {model_name}", 298 | "", 299 | "", 300 | schema_class 301 | ]) 302 | schemas_file.write_text(content) 303 | 304 | return True 305 | 306 | def update_api_py(self, app_dir, model_name, fields): 307 | """Update api.py file with new router content.""" 308 | api_file = app_dir / "api.py" 309 | router_content = self.generate_router_content(model_name, fields) 310 | 311 | if api_file.exists(): 312 | content = api_file.read_text() 313 | 314 | if f"def list_{model_name.lower()}s" in content: 315 | self.stdout.write(self.style.WARNING(f"Routes for {model_name} already exist in {api_file}")) 316 | return False 317 | 318 | model_import = f"from .models import {model_name}" 319 | schema_import = f"from .schemas import {model_name}Schema, {model_name}InputSchema" 320 | 321 | if model_name not in re.findall(r'from .models import (.+)', content): 322 | if "from .models import " in content: 323 | content = re.sub(r'from .models import (.+)', f'from .models import \\1, {model_name}', content) 324 | else: 325 | content = content.replace("from ninja import Router", f"from ninja import Router\n{model_import}") 326 | 327 | if f"{model_name}Schema" not in content: 328 | if "from .schemas import " in content: 329 | content = re.sub(r'from .schemas import (.+)', 330 | f'from .schemas import \\1, {model_name}Schema, {model_name}InputSchema', content) 331 | else: 332 | content = content.replace("from ninja import Router", f"from ninja import Router\n{schema_import}") 333 | 334 | content += "\n" + router_content 335 | api_file.write_text(content) 336 | else: 337 | content = '\n'.join([ 338 | "from http import HTTPStatus", 339 | "from django.shortcuts import get_object_or_404", 340 | "from ninja import Router", 341 | f"from .models import {model_name}", 342 | f"from .schemas import {model_name}Schema, {model_name}InputSchema", 343 | "", 344 | "", 345 | "router = Router()", 346 | "", 347 | router_content 348 | ]) 349 | api_file.write_text(content) 350 | 351 | return True 352 | 353 | def update_admin_py(self, app_dir, model_name, fields): 354 | """Update admin.py file with new admin class.""" 355 | admin_file = app_dir / "admin.py" 356 | admin_class = self.generate_admin_class(model_name, fields) 357 | 358 | if admin_file.exists(): 359 | content = admin_file.read_text() 360 | 361 | if f"class {model_name}Admin" in content: 362 | self.stdout.write(self.style.WARNING(f"Admin class for {model_name} already exists in {admin_file}")) 363 | return False 364 | 365 | if f"from .models import {model_name}" not in content: 366 | if "from .models import " in content: 367 | content = re.sub(r'from .models import (.+)', f'from .models import \\1, {model_name}', content) 368 | else: 369 | content += f"\nfrom .models import {model_name}\n" 370 | 371 | content += "\n" + admin_class 372 | admin_file.write_text(content) 373 | else: 374 | content = '\n'.join([ 375 | "from django.contrib import admin", 376 | f"from .models import {model_name}", 377 | "", 378 | "", 379 | admin_class 380 | ]) 381 | admin_file.write_text(content) 382 | 383 | return True 384 | 385 | def extract_model_fields(self, app_name, model_name): 386 | """Extract fields from an existing model by parsing the models.py file.""" 387 | # Use BASE_DIR to find the correct path 388 | # Apps are inside the project directory 389 | project_name = self.get_project_name() 390 | base_dir = Path(settings.BASE_DIR) 391 | models_file = base_dir / project_name / app_name / "models.py" 392 | 393 | if not models_file.exists(): 394 | raise CommandError(f"File {models_file} not found.") 395 | 396 | try: 397 | content = models_file.read_text() 398 | 399 | pattern = rf"class\s+{model_name}\s*\([^)]*\):(.+?)(?:class\s+\w+|\Z)" 400 | match = re.search(pattern, content, re.DOTALL) 401 | 402 | if not match: 403 | raise CommandError(f"Model '{model_name}' not found in {models_file}") 404 | 405 | model_content = match.group(1) 406 | 407 | field_pattern = r"^\s*(\w+)\s*=\s*models\.(\w+)\s*\(" 408 | field_matches = re.finditer(field_pattern, model_content, re.MULTILINE) 409 | 410 | fields = [] 411 | for match in field_matches: 412 | field_name = match.group(1) 413 | field_type = match.group(2).lower() 414 | 415 | if field_name not in ['id', 'objects']: 416 | fields.append((field_name, field_type)) 417 | 418 | if not fields: 419 | self.stdout.write(self.style.WARNING(f"No fields found in model '{model_name}'.")) 420 | else: 421 | self.stdout.write(self.style.SUCCESS(f"Extracted {len(fields)} fields from {models_file}:")) 422 | for name, field_type in fields: 423 | self.stdout.write(f" - {name}: {field_type}") 424 | 425 | return fields 426 | 427 | except Exception as e: 428 | raise CommandError(f"Failed to parse model file: {str(e)}") 429 | 430 | def generate_from_existing_model(self, app_name, model_name): 431 | """Generate schemas, API, and admin for an existing model.""" 432 | self.stdout.write(f"Generating from existing model '{model_name}' in app '{app_name}'...") 433 | 434 | fields = self.extract_model_fields(app_name, model_name) 435 | 436 | if not fields: 437 | self.stdout.write(self.style.WARNING(f"No fields found for model '{model_name}' in app '{app_name}'.")) 438 | return 439 | 440 | # Use BASE_DIR to find the correct path 441 | # Apps are inside the project directory 442 | project_name = self.get_project_name() 443 | base_dir = Path(settings.BASE_DIR) 444 | app_dir = base_dir / project_name / app_name 445 | 446 | schemas_updated = self.update_schemas_py(app_dir, model_name, fields) 447 | api_updated = self.update_api_py(app_dir, model_name, fields) 448 | admin_updated = self.update_admin_py(app_dir, model_name, fields) 449 | 450 | self.stdout.write(self.style.SUCCESS(f"Successfully generated files for model '{model_name}' in app '{app_name}':")) 451 | 452 | app_path = f"{project_name}/{app_name}" 453 | if schemas_updated: 454 | self.stdout.write(f" - {app_path}/schemas.py (updated)") 455 | if api_updated: 456 | self.stdout.write(f" - {app_path}/api.py (updated)") 457 | if admin_updated: 458 | self.stdout.write(f" - {app_path}/admin.py (updated)") 459 | 460 | def update_project_settings(self, app_name): 461 | """Add app to INSTALLED_APPS in settings.py.""" 462 | project_name = self.get_project_name() 463 | if not project_name: 464 | self.stdout.write(self.style.WARNING("Could not determine project name. Skipping settings.py update.")) 465 | return False 466 | 467 | # Use BASE_DIR to find the correct path 468 | base_dir = Path(settings.BASE_DIR) 469 | settings_file = base_dir / project_name / "settings.py" 470 | 471 | if not settings_file.exists(): 472 | self.stdout.write(self.style.WARNING(f"Settings file {settings_file} not found. Skipping settings.py update.")) 473 | return False 474 | 475 | content = settings_file.read_text() 476 | app_path = f"'{project_name}.{app_name}'" 477 | 478 | if app_path in content: 479 | self.stdout.write(self.style.WARNING(f"App {app_path} already in INSTALLED_APPS")) 480 | return False 481 | 482 | # Find INSTALLED_APPS and add the new app 483 | if "# my apps" in content: 484 | # Add after the "# my apps" comment 485 | content = content.replace("# my apps", f"# my apps\n {app_path},") 486 | settings_file.write_text(content) 487 | self.stdout.write(self.style.SUCCESS(f"Added {app_path} to INSTALLED_APPS in {settings_file}")) 488 | return True 489 | elif "INSTALLED_APPS = [" in content: 490 | # Add at the end of INSTALLED_APPS 491 | pattern = r'(INSTALLED_APPS\s*=\s*\[.*?)(\])' 492 | replacement = rf'\1 {app_path},\n\2' 493 | new_content = re.sub(pattern, replacement, content, flags=re.DOTALL) 494 | settings_file.write_text(new_content) 495 | self.stdout.write(self.style.SUCCESS(f"Added {app_path} to INSTALLED_APPS in {settings_file}")) 496 | return True 497 | else: 498 | self.stdout.write(self.style.WARNING("Could not find INSTALLED_APPS in settings.py")) 499 | return False 500 | 501 | def update_project_api(self, app_name): 502 | """Add router to main api.py file.""" 503 | project_name = self.get_project_name() 504 | if not project_name: 505 | self.stdout.write(self.style.WARNING("Could not determine project name. Skipping api.py update.")) 506 | return False 507 | 508 | # Use BASE_DIR to find the correct path 509 | base_dir = Path(settings.BASE_DIR) 510 | api_file = base_dir / project_name / "api.py" 511 | 512 | if not api_file.exists(): 513 | self.stdout.write(self.style.WARNING(f"API file {api_file} not found. Skipping api.py update.")) 514 | return False 515 | 516 | content = api_file.read_text() 517 | router_line = f"api.add_router('', '{project_name}.{app_name}.api.router')" 518 | 519 | if f"{project_name}.{app_name}.api.router" in content: 520 | self.stdout.write(self.style.WARNING(f"Router for {app_name} already exists in {api_file}")) 521 | return False 522 | 523 | # Add router line before the comment "# Adiciona mais apps aqui" or at the end 524 | if "# Adiciona mais apps aqui" in content: 525 | content = content.replace("# Adiciona mais apps aqui", f"{router_line}\n# Adiciona mais apps aqui") 526 | else: 527 | content += f"\n{router_line}\n" 528 | 529 | api_file.write_text(content) 530 | self.stdout.write(self.style.SUCCESS(f"Added router for {app_name} to {api_file}")) 531 | return True 532 | 533 | def update_app_config(self, app_dir, app_name): 534 | """Update apps.py with correct name format.""" 535 | project_name = self.get_project_name() 536 | if not project_name: 537 | self.stdout.write(self.style.WARNING("Could not determine project name. Using app_name only in apps.py")) 538 | full_name = app_name 539 | else: 540 | full_name = f"{project_name}.{app_name}" 541 | 542 | apps_file = app_dir / "apps.py" 543 | if not apps_file.exists(): 544 | return False 545 | 546 | content = apps_file.read_text() 547 | # Update the name attribute to use project_name.app_name format 548 | content = re.sub( 549 | r"(name\s*=\s*')[^']*(')", 550 | rf"\1{full_name}\2", 551 | content 552 | ) 553 | apps_file.write_text(content) 554 | self.stdout.write(self.style.SUCCESS(f"Updated apps.py with name = '{full_name}'")) 555 | return True 556 | 557 | def create_or_update_app_structure(self, app_name, model_name, fields): 558 | """Create or update the app directory structure and files.""" 559 | # Use BASE_DIR to find the correct path 560 | # Apps should be created inside the project directory (where settings.py is) 561 | project_name = self.get_project_name() 562 | base_dir = Path(settings.BASE_DIR) 563 | app_dir = base_dir / project_name / app_name 564 | app_dir.mkdir(exist_ok=True) 565 | 566 | init_file = app_dir / "__init__.py" 567 | init_file.touch() 568 | 569 | models_updated = self.update_models_py(app_dir, model_name, fields) 570 | schemas_updated = self.update_schemas_py(app_dir, model_name, fields) 571 | api_updated = self.update_api_py(app_dir, model_name, fields) 572 | admin_updated = self.update_admin_py(app_dir, model_name, fields) 573 | 574 | apps_file = app_dir / "apps.py" 575 | app_config_created = False 576 | if not apps_file.exists(): 577 | project_name = self.get_project_name() 578 | full_name = f"{project_name}.{app_name}" if project_name else app_name 579 | class_name = ''.join(word.capitalize() for word in app_name.split('_')) 580 | apps_content = '\n'.join([ 581 | "from django.apps import AppConfig", 582 | "", 583 | "", 584 | f"class {class_name}Config(AppConfig):", 585 | " default_auto_field = 'django.db.models.BigAutoField'", 586 | f" name = '{full_name}'", 587 | "", 588 | " def ready(self):", 589 | " pass # Import signals and other initialization here", 590 | "" 591 | ]) 592 | apps_file.write_text(apps_content) 593 | app_config_created = True 594 | else: 595 | # Update existing apps.py with correct name format 596 | self.update_app_config(app_dir, app_name) 597 | 598 | # Update project-level files 599 | self.update_project_settings(app_name) 600 | self.update_project_api(app_name) 601 | 602 | action = 'updated' if any([models_updated, schemas_updated, api_updated, admin_updated]) else 'added' 603 | self.stdout.write(self.style.SUCCESS(f"Successfully {action} model '{model_name}' to app '{app_name}'")) 604 | self.stdout.write("Files:") 605 | app_path = f"{project_name}/{app_name}" 606 | self.stdout.write(f" - {app_path}/__init__.py") 607 | self.stdout.write(f" - {app_path}/models.py") 608 | self.stdout.write(f" - {app_path}/schemas.py") 609 | self.stdout.write(f" - {app_path}/api.py") 610 | self.stdout.write(f" - {app_path}/admin.py") 611 | self.stdout.write(f" - {app_path}/apps.py") 612 | --------------------------------------------------------------------------------