├── .commitlintrc.json ├── .github └── workflows │ ├── codesee-arch-diagram.yml │ ├── pypi-publish.yml │ └── python-tests.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .versionrc.json ├── Dockerfile ├── LICENSE ├── MANIFEST.in ├── README.md ├── docs ├── Makefile ├── cli │ ├── commands │ │ ├── destroy.ipynb │ │ ├── generate.ipynb │ │ └── new.ipynb │ └── readme.ipynb ├── conf.py ├── index.rst └── make.bat ├── pyproject.toml ├── src ├── __init__.py └── django_clite │ ├── __init__.py │ ├── cli.py │ ├── commands │ ├── __init__.py │ ├── callbacks.py │ ├── command_defaults.py │ ├── destroy │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── dockerfile.py │ │ ├── fixtures.py │ │ ├── forms.py │ │ ├── main.py │ │ ├── management.py │ │ ├── managers.py │ │ ├── models.py │ │ ├── serializers.py │ │ ├── signals.py │ │ ├── tags.py │ │ ├── template.py │ │ ├── tests.py │ │ ├── validators.py │ │ ├── views.py │ │ └── viewsets.py │ ├── generate │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── dockerfile.py │ │ ├── fixtures.py │ │ ├── forms.py │ │ ├── main.py │ │ ├── management.py │ │ ├── managers.py │ │ ├── models.py │ │ ├── serializers.py │ │ ├── signals.py │ │ ├── tags.py │ │ ├── template.py │ │ ├── tests.py │ │ ├── validators.py │ │ ├── views.py │ │ └── viewsets.py │ └── new │ │ ├── __init__.py │ │ ├── app.py │ │ ├── defaults │ │ ├── __init__.py │ │ ├── app.py │ │ └── project.py │ │ ├── main.py │ │ └── project.py │ ├── constants.py │ ├── core │ ├── __init__.py │ ├── field_parser │ │ ├── __init__.py │ │ └── factory.py │ ├── git │ │ ├── __init__.py │ │ ├── git.py │ │ └── protocols.py │ └── logger.py │ ├── decorators │ ├── __init__.py │ └── scope.py │ ├── extensions │ ├── __init__.py │ ├── aliased.py │ ├── combined.py │ └── discoverable.py │ ├── template_files │ ├── __init__.py │ ├── admin │ │ ├── admin.tpl │ │ └── inline.tpl │ ├── app │ │ ├── 404.tpl │ │ ├── 500.tpl │ │ ├── LICENSE.tpl │ │ ├── MANIFEST.tpl │ │ ├── README.tpl │ │ ├── __init__.py │ │ ├── admin-init.tpl │ │ ├── apps.tpl │ │ ├── gitignore.tpl │ │ ├── init.tpl │ │ ├── pyproject.tpl │ │ ├── router.tpl │ │ ├── router_init.tpl │ │ ├── router_urls.tpl │ │ ├── routes.tpl │ │ └── urls.tpl │ ├── docker │ │ ├── docker-compose.tpl │ │ ├── docker-entrypoint.tpl │ │ ├── dockerfile.tpl │ │ └── dockerignore.tpl │ ├── fixture.tpl │ ├── form.tpl │ ├── github │ │ ├── ci.tpl │ │ └── pull_request_template.tpl │ ├── kubernetes │ │ ├── configmap.tpl │ │ ├── deployment.tpl │ │ ├── ingress.tpl │ │ └── service.tpl │ ├── management.tpl │ ├── models │ │ ├── manager.tpl │ │ ├── model.tpl │ │ ├── signal.tpl │ │ ├── test.tpl │ │ └── validator.tpl │ ├── project │ │ ├── README.tpl │ │ ├── __init__.py │ │ ├── api.tpl │ │ ├── app_json.tpl │ │ ├── celery.tpl │ │ ├── celery_init.tpl │ │ ├── celery_tasks.tpl │ │ ├── cli-config_json.tpl │ │ ├── cli-config_yaml.tpl │ │ ├── dokku_checks.tpl │ │ ├── dokku_scale.tpl │ │ ├── env.tpl │ │ ├── github_ci.tpl │ │ ├── gitignore.tpl │ │ ├── pipfile.tpl │ │ ├── procfile.tpl │ │ ├── requirements.tpl │ │ ├── settings.tpl │ │ ├── storage.tpl │ │ ├── travis-yml.tpl │ │ └── urls.tpl │ ├── serializer.tpl │ ├── shared │ │ └── init.tpl │ ├── templates │ │ ├── create.tpl │ │ ├── detail.tpl │ │ ├── list.tpl │ │ ├── template.tpl │ │ └── update.tpl │ ├── templatetags │ │ └── tag.tpl │ ├── views │ │ ├── create.tpl │ │ ├── detail.tpl │ │ ├── list.tpl │ │ ├── update.tpl │ │ └── view.tpl │ └── viewsets │ │ ├── test.tpl │ │ └── viewset.tpl │ └── utils.py ├── tests ├── __init__.py ├── commands │ ├── __init__.py │ ├── generators │ │ ├── __init__.py │ │ └── test_generators.py │ └── new │ │ ├── __init__.py │ │ └── test_new.py ├── core │ ├── __init__.py │ └── field_parser │ │ ├── __init__.py │ │ ├── test_factory.py │ │ └── test_field_parser.py ├── test_utils.py └── tmp │ └── .keep └── yarn.lock /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@commitlint/config-conventional"] 3 | } -------------------------------------------------------------------------------- /.github/workflows/codesee-arch-diagram.yml: -------------------------------------------------------------------------------- 1 | # This workflow was added by CodeSee. Learn more at https://codesee.io/ 2 | # This is v2.0 of this workflow file 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request_target: 8 | types: [opened, synchronize, reopened] 9 | 10 | name: CodeSee 11 | 12 | permissions: read-all 13 | 14 | jobs: 15 | codesee: 16 | runs-on: ubuntu-latest 17 | continue-on-error: true 18 | name: Analyze the repo with CodeSee 19 | steps: 20 | - uses: Codesee-io/codesee-action@v2 21 | with: 22 | codesee-token: ${{ secrets.CODESEE_ARCH_DIAG_API_TOKEN }} 23 | -------------------------------------------------------------------------------- /.github/workflows/pypi-publish.yml: -------------------------------------------------------------------------------- 1 | name: publish 2 | 3 | on: 4 | release: 5 | types: 6 | - released 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout code 13 | id: checkout_code 14 | uses: actions/checkout@master 15 | 16 | - name: Set up Python 17 | uses: actions/setup-python@v4 18 | with: 19 | python-version: '3.x' 20 | 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install setuptools wheel twine build 25 | 26 | - name: Build package 27 | run: python -m build 28 | 29 | - name: Publish package 30 | env: 31 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 32 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 33 | run: twine upload dist/* 34 | -------------------------------------------------------------------------------- /.github/workflows/python-tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - '*' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | os: [ubuntu-latest, macos-latest, windows-latest] 14 | python-version: [ "pypy3.9", "pypy3.10", "3.9", "3.10", "3.11", "3.12" ] 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: Set up Python ${{ matrix.python-version }} 20 | uses: actions/setup-python@v4 21 | with: 22 | python-version: ${{ matrix.python-version }} 23 | 24 | - name: Display Python version 25 | run: python -c "import sys; print(sys.version)" 26 | 27 | - name: Install dependencies 28 | run: python -m pip install --upgrade pip setuptools wheel coverage ruff 29 | 30 | - name: Lint with Ruff 31 | run: | 32 | ruff --output-format=github . 33 | continue-on-error: true 34 | 35 | - name: Run tests 36 | run: | 37 | pip install -e . 38 | python -m unittest discover -s tests/ 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode 3 | *.egg 4 | *.egg* 5 | *.pyc 6 | pycache 7 | *wheel* 8 | 9 | # Test project 10 | A 11 | django_test* 12 | *dummy* 13 | ignore* 14 | _ignore/ 15 | 16 | # Byte-compiled / optimized / DLL files 17 | __pycache__/ 18 | *.py[cod] 19 | *$py.class 20 | 21 | # C extensions 22 | *.so 23 | 24 | # Distribution / packaging 25 | .Python 26 | build/ 27 | develop-eggs/ 28 | dist/ 29 | downloads/ 30 | eggs/ 31 | .eggs/ 32 | lib/ 33 | lib64/ 34 | parts/ 35 | sdist/ 36 | var/ 37 | wheels/ 38 | *.egg-info/ 39 | .installed.cfg 40 | *.egg 41 | MANIFEST 42 | 43 | # PyInstaller 44 | # Usually these files are written by a python script from a template 45 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 46 | *.manifest 47 | *.spec 48 | 49 | # Installer logs 50 | pip-log.txt 51 | pip-delete-this-directory.txt 52 | 53 | # Unit test / coverage reports 54 | htmlcov/ 55 | .tox/ 56 | .coverage 57 | .coverage.* 58 | .cache 59 | nosetests.xml 60 | coverage.xml 61 | *.cover 62 | .hypothesis/ 63 | .pytest_cache/ 64 | 65 | # Translations 66 | *.mo 67 | *.pot 68 | 69 | # Django stuff: 70 | *.log 71 | local_settings.py 72 | db.sqlite3 73 | secrets.py 74 | credentials.py 75 | 76 | # Keys + Confidential 77 | credentials* 78 | keys* 79 | secrets* 80 | *pipelines.yml 81 | 82 | # Flask stuff: 83 | instance/ 84 | .webassets-cache 85 | 86 | # Scrapy stuff: 87 | .scrapy 88 | 89 | # Sphinx documentation 90 | docs/_build/ 91 | 92 | # PyBuilder 93 | target/ 94 | 95 | # Jupyter Notebook 96 | .ipynb_checkpoints 97 | 98 | # pyenv 99 | .python-version 100 | 101 | # celery beat schedule file 102 | celerybeat-schedule 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | 129 | # Node 130 | node_modules 131 | 132 | .DS_Store 133 | 134 | tests/tmp/ 135 | !tests/tmp/.keep 136 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | #npx --no-install commitlint --edit "$1" 4 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | python -m unittest discover tests 5 | -------------------------------------------------------------------------------- /.versionrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "types": [ 3 | {"type": "feat", "section": "Features"}, 4 | {"type": "fix", "section": "Bug Fixes"}, 5 | {"type": "chore", "hidden": true}, 6 | {"type": "docs", "hidden": true}, 7 | {"type": "style", "hidden": true}, 8 | {"type": "refactor", "hidden": true}, 9 | {"type": "perf", "hidden": true}, 10 | {"type": "test", "hidden": true} 11 | ], 12 | "commitUrlFormat": "https://github.com/oleoneto/django-clite/commits/{{hash}}", 13 | "compareUrlFormat": "https://github.com/oleoneto/django-clite/compare/{{previousTag}}...{{currentTag}}" 14 | } 15 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Base image 2 | FROM python:3.13.0a4-alpine 3 | 4 | LABEL MAINTAINER="Leo Neto" 5 | 6 | # Working directory 7 | WORKDIR /app 8 | 9 | # Environment variables 10 | ENV PYTHONDONTWRITEBYTECODE 1 11 | ENV PYTHONUNBUFFERED 1 12 | ENV LANG C.UTF-8 13 | 14 | # Copy other files to docker container 15 | COPY . . 16 | 17 | # Install dependencies 18 | RUN apk add gcc musl-dev linux-headers && pip install -e . 19 | 20 | CMD ["django-clite"] 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019, Leo Neto 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include INSTALL 2 | include LICENSE 3 | include README.md 4 | include MANIFEST.in 5 | graft src 6 | graft tests 7 | global-exclude __pycache__ 8 | global-exclude *.py[co] 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # django-clite 2 | 3 | A CLI tool that handles creating and managing Django projects 4 | 5 | ![publish](https://github.com/oleoneto/django-clite/workflows/publish/badge.svg?branch=master) 6 | ![PyPI - Package](https://img.shields.io/pypi/v/django-clite) 7 | ![PyPI - Python](https://img.shields.io/pypi/pyversions/django-clite) 8 | ![PyPI - License](https://img.shields.io/pypi/l/django-clite) 9 | ![PyPI - Downloads](https://img.shields.io/pypi/dm/django-clite) 10 | 11 | 12 | - [django-clite](#django-clite) 13 | - [Installation](#installation) 14 | - [Extending the CLI](#extending-the-cli) 15 | - [Dependencies](#dependencies) 16 | - [Interactive Docs](#interactive-docs) 17 | - [To Do](#to-do) 18 | - [Pull requests](#pull-requests) 19 | - [LICENSE](#license) 20 | 21 | ## Installation 22 | Install via [pip](https://pypi.org/project/django-clite/): 23 | ```bash 24 | pip install django-clite 25 | ``` 26 | 27 | After installation, the CLI will expose the binary with the name: 28 | ``` 29 | django-clite 30 | ``` 31 | 32 | ## Extending the CLI 33 | 34 | Currently, there are two main ways of extending the functionality of the CLI: 35 | 1. Adding your own commands/plugins 36 | 2. Overriding the provided resource templates 37 | 38 | ### Including your own commands 39 | 40 | If you would like to extend the functionality of this CLI, you can include your own `plugins/commands` by 41 | setting an environment variable: `DJANGO_CLITE_PLUGINS`. Simply set this variable to the path where your plugins are. 42 | 43 | Plugin commands are auto-discovered if they are placed under the plugins directory, 44 | but please be sure to do the following for this to work: 45 | 1. **Name your package and click command the same**. That is, a package called `get`, for example, should define a `get` command. 46 | 2. **Place the command definition within the package's `main.py` module**. For example: 47 | ```python 48 | # get/main.py 49 | import click 50 | 51 | 52 | @click.command() 53 | @click.pass_context 54 | def get(ctx): 55 | pass 56 | ``` 57 | 3. **Sub-commands should be added to the top-most command group in the package's `main.py` module.** 58 | ```python 59 | # get/main.py 60 | import click 61 | 62 | 63 | @click.group() # <- group 64 | @click.pass_context 65 | def get(ctx): 66 | pass 67 | 68 | 69 | @click.command() 70 | @click.pass_context 71 | def foo(ctx): 72 | pass 73 | 74 | 75 | get.add_command(foo) 76 | ``` 77 | 4. **Access your commands via your top-most command group.** 78 | ``` 79 | django-clite get foo 80 | ``` 81 | 82 | **NOTE:** If you would like to skip a plugin/command from being auto-discovered, simply rename the package by either 83 | prepending or appending any number of underscores (`_`). Any code contained within the package will be ignored. 84 | 85 | ### Overriding the templates 86 | 87 | The flag `--templates-dir` can be used to configure an additional path wherein the CLI can look for resource templates. 88 | Alternatively, you can use the environment variable `DJANGO_CLITE_TEMPLATES_DIR` for the same purpose. 89 | 90 | Take a look at the [template files directory](django_clite/cli/template_files) for a reference of what files can be overriden. The 91 | paths of the templates you wish to override need to match the provided template. For example, if you wish to override the 92 | model template, which is defined under [`src/cli/template_files/models/model.tpl`](django_clite/cli/template_files/models/model.tpl), 93 | you should define your own model template under your desired directory, i.e `/path/to/templates/models/model.tpl`. 94 | 95 | ## Development 96 | 97 | ### Install from source: 98 | ``` 99 | git clone https://github.com/oleoneto/django-clite.git 100 | cd django-clite 101 | pip install --editable . 102 | ``` 103 | 104 | ### Dependencies 105 | Check out [pyproject.toml](pyproject.toml) for all installation dependencies. 106 | 107 | ## Interactive Docs 108 | In order to maintain consistency in our documentation of all the different commands and features of the CLI, 109 | we've decided to move the [README](docs/cli/readme.ipynb) to a series of Jupyter notebooks which you can explore per command under the [docs](docs) directory. 110 | 111 | ## To Do 112 | [Check out our open issues](https://github.com/oleoneto/django-clite/issues). 113 | 114 | ## Pull requests 115 | Found a bug? See a typo? Have an idea for new command? 116 | Feel free to submit a pull request with your contributions. They are much welcome and appreciated. 117 | 118 | ## LICENSE 119 | **django-clite** is [BSD Licensed](LICENSE). 120 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/cli/commands/destroy.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Destroy ❌\n", 8 | "### The destroyer is accessible through the `destroy` command (abbreviated `d`)." 9 | ] 10 | }, 11 | { 12 | "cell_type": "code", 13 | "execution_count": 1, 14 | "metadata": {}, 15 | "outputs": [ 16 | { 17 | "name": "stdout", 18 | "output_type": "stream", 19 | "text": [ 20 | "Usage: django-clite destroy [OPTIONS] COMMAND [ARGS]...\n", 21 | "\n", 22 | " Destroy application resources.\n", 23 | "\n", 24 | "Options:\n", 25 | " --help Show this message and exit.\n", 26 | "\n", 27 | "Commands:\n", 28 | " admin Destroy an admin model.\n", 29 | " admin-inline Destroy an inline admin model.\n", 30 | " command Destroy an application command.\n", 31 | " fixture Destroy model fixtures.\n", 32 | " form Destroy a form.\n", 33 | " manager Destroy a model manager.\n", 34 | " model Destroy a model.\n", 35 | " serializer Destroy a serializer for a given model.\n", 36 | " signal Destroy a signal.\n", 37 | " tag Generate a template tag.\n", 38 | " template Destroy an html template.\n", 39 | " test Destroy TestCases.\n", 40 | " validator Destroy a validator.\n", 41 | " view Destroy a view function or class.\n", 42 | " viewset Destroy a viewset for a serializable model.\n" 43 | ] 44 | } 45 | ], 46 | "source": [ 47 | "! django-clite destroy --help" 48 | ] 49 | }, 50 | { 51 | "cell_type": "markdown", 52 | "metadata": {}, 53 | "source": [ 54 | "## Destroying Models" 55 | ] 56 | }, 57 | { 58 | "cell_type": "code", 59 | "execution_count": 2, 60 | "metadata": {}, 61 | "outputs": [ 62 | { 63 | "name": "stdout", 64 | "output_type": "stream", 65 | "text": [ 66 | "Usage: django-clite destroy model [OPTIONS] NAME\n", 67 | "\n", 68 | " Destroy a model.\n", 69 | "\n", 70 | "Options:\n", 71 | " --api Destroy only related api resources\n", 72 | " --full Destroy all related resources\n", 73 | " --admin Destroy admin model\n", 74 | " --fixtures Destroy model fixture\n", 75 | " --form Destroy model form\n", 76 | " --serializers Destroy serializers\n", 77 | " --templates Destroy templates\n", 78 | " --tests Destroy tests\n", 79 | " --views Destroy views\n", 80 | " --viewsets Destroy viewsets\n", 81 | " --help Show this message and exit.\n" 82 | ] 83 | } 84 | ], 85 | "source": [ 86 | "! django-clite destroy model --help" 87 | ] 88 | }, 89 | { 90 | "cell_type": "markdown", 91 | "metadata": {}, 92 | "source": [ 93 | "## Destroying Resources" 94 | ] 95 | }, 96 | { 97 | "cell_type": "markdown", 98 | "metadata": {}, 99 | "source": [ 100 | "Suppose you have a resource (consisting of admin, model, serializer, view, viewset...), you can delete all related files by running the `destroy resource` command.\n", 101 | "\n", 102 | "The following example shows how to destroy a resource called `Article`, which we'll create with the `generator`." 103 | ] 104 | }, 105 | { 106 | "cell_type": "code", 107 | "execution_count": null, 108 | "metadata": {}, 109 | "outputs": [], 110 | "source": [ 111 | "! django-clite destroy resource article" 112 | ] 113 | } 114 | ], 115 | "metadata": { 116 | "file_extension": ".py", 117 | "kernelspec": { 118 | "display_name": ".venv", 119 | "language": "python", 120 | "name": "python3" 121 | }, 122 | "language_info": { 123 | "codemirror_mode": { 124 | "name": "ipython", 125 | "version": 3 126 | }, 127 | "file_extension": ".py", 128 | "mimetype": "text/x-python", 129 | "name": "python", 130 | "nbconvert_exporter": "python", 131 | "pygments_lexer": "ipython3", 132 | "version": "3.11.5" 133 | }, 134 | "mimetype": "text/x-python", 135 | "name": "python", 136 | "npconvert_exporter": "python", 137 | "orig_nbformat": 2, 138 | "pygments_lexer": "ipython3", 139 | "version": 3 140 | }, 141 | "nbformat": 4, 142 | "nbformat_minor": 2 143 | } 144 | -------------------------------------------------------------------------------- /docs/cli/commands/generate.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Generate 🗂\n", 8 | "### The generator is accessible through the `generate` command (abbreviated `g`)." 9 | ] 10 | }, 11 | { 12 | "cell_type": "code", 13 | "execution_count": 13, 14 | "metadata": {}, 15 | "outputs": [ 16 | { 17 | "name": "stdout", 18 | "output_type": "stream", 19 | "text": [ 20 | "Usage: django-clite generate [OPTIONS] COMMAND [ARGS]...\n", 21 | "\n", 22 | " Create application resources.\n", 23 | "\n", 24 | "Options:\n", 25 | " -d, --directory PATH Specify the path to the project's management file.\n", 26 | " --project PATH Project name.\n", 27 | " --app PATH Application name.\n", 28 | " --help Show this message and exit.\n", 29 | "\n", 30 | "Commands:\n", 31 | " admin Generate an admin model.\n", 32 | " admin-inline Generate an inline admin model.\n", 33 | " command Generate an application command.\n", 34 | " fixture Generate model fixtures.\n", 35 | " form Generate a form.\n", 36 | " manager Generate a model manager.\n", 37 | " model Generates a model under the models directory.\n", 38 | " serializer Generate a serializer for a given model.\n", 39 | " signal Generate a signal.\n", 40 | " tag Generate a template tag.\n", 41 | " template Generate an html template.\n", 42 | " test Generate TestCases.\n", 43 | " validator Generate a validator.\n", 44 | " view Generate a view function or class.\n", 45 | " viewset Generate a viewset for a serializable model.\n" 46 | ] 47 | } 48 | ], 49 | "source": [ 50 | "! django-clite generate --help" 51 | ] 52 | }, 53 | { 54 | "cell_type": "markdown", 55 | "metadata": {}, 56 | "source": [ 57 | "## Generating Models" 58 | ] 59 | }, 60 | { 61 | "cell_type": "code", 62 | "execution_count": 2, 63 | "metadata": {}, 64 | "outputs": [ 65 | { 66 | "name": "stdout", 67 | "output_type": "stream", 68 | "text": [ 69 | "Usage: django-clite generate model [OPTIONS] NAME [FIELDS]...\n", 70 | "\n", 71 | "Options:\n", 72 | " -a, --abstract Creates an abstract model type\n", 73 | " --api Adds only related api resources\n", 74 | " --full Adds all related resources\n", 75 | " --admin Register admin model\n", 76 | " --fixtures Create model fixture\n", 77 | " --form Create model form\n", 78 | " --serializers Create serializers\n", 79 | " --templates Create templates\n", 80 | " --tests Create tests\n", 81 | " --views Create views\n", 82 | " --viewsets Create viewsets\n", 83 | " --skip-import Do not import in __init__ module\n", 84 | " --help Show this message and exit.\n" 85 | ] 86 | } 87 | ], 88 | "source": [ 89 | "! django-clite generate model --help" 90 | ] 91 | }, 92 | { 93 | "cell_type": "markdown", 94 | "metadata": {}, 95 | "source": [ 96 | "In order to generate a model, specify the type identifier and then the name of the attribute field. Type identifiers are abbreviated to a more generic name that omits the word `Field`. The input here is case-insensitive, but the fields will be properly CamelCased in the corresponding Python file as in the example below:" 97 | ] 98 | }, 99 | { 100 | "cell_type": "code", 101 | "execution_count": null, 102 | "metadata": {}, 103 | "outputs": [], 104 | "source": [ 105 | "! django-clite generate model album text:title image:artwork bool:is_compilation" 106 | ] 107 | }, 108 | { 109 | "cell_type": "markdown", 110 | "metadata": {}, 111 | "source": [ 112 | "This would add the following model `album.py` under the `models` directory within the corresponding app. If the command is run outside of an application, an error will be raised." 113 | ] 114 | }, 115 | { 116 | "cell_type": "markdown", 117 | "metadata": {}, 118 | "source": [ 119 | "Note the presence of the `--inherits` flag. You can specify a base model and the generated model will extend it. For example (from within the music directory):" 120 | ] 121 | }, 122 | { 123 | "cell_type": "code", 124 | "execution_count": null, 125 | "metadata": {}, 126 | "outputs": [], 127 | "source": [ 128 | "! django-clite generate model ep --inherits album" 129 | ] 130 | }, 131 | { 132 | "cell_type": "markdown", 133 | "metadata": {}, 134 | "source": [ 135 | "**Defaults**\n", 136 | "\n", 137 | "As one can see, `class Meta` and `_str_` are added to a model by default along with `uuid`, `slug`, `created_at` and `updated_at` fields.\n", 138 | "The `db_table` name is inferred from the name of the app and the current model while the ordering attribute is defined based on the default `created_at` field." 139 | ] 140 | }, 141 | { 142 | "cell_type": "markdown", 143 | "metadata": {}, 144 | "source": [ 145 | "**Relationships**\n", 146 | "\n", 147 | "If a relationship identifier is passed, the attribute name will be used as the name of the model it relates to.\n", 148 | "Specifying a relationship also checks the current app scope for the specified related model. If such model does not exist in scope, the CLI will prompt you to create the missing model. How to invoke the command:" 149 | ] 150 | }, 151 | { 152 | "cell_type": "code", 153 | "execution_count": null, 154 | "metadata": {}, 155 | "outputs": [], 156 | "source": [ 157 | "! django-clite generate model track char:title" 158 | ] 159 | } 160 | ], 161 | "metadata": { 162 | "file_extension": ".py", 163 | "kernelspec": { 164 | "display_name": ".venv", 165 | "language": "python", 166 | "name": "python3" 167 | }, 168 | "language_info": { 169 | "codemirror_mode": { 170 | "name": "ipython", 171 | "version": 3 172 | }, 173 | "file_extension": ".py", 174 | "mimetype": "text/x-python", 175 | "name": "python", 176 | "nbconvert_exporter": "python", 177 | "pygments_lexer": "ipython3", 178 | "version": "3.11.5" 179 | }, 180 | "mimetype": "text/x-python", 181 | "name": "python", 182 | "npconvert_exporter": "python", 183 | "orig_nbformat": 2, 184 | "pygments_lexer": "ipython3", 185 | "version": 3 186 | }, 187 | "nbformat": 4, 188 | "nbformat_minor": 2 189 | } 190 | -------------------------------------------------------------------------------- /docs/cli/commands/new.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# New 🏗\n", 8 | "### Create Django projects and their respective applications." 9 | ] 10 | }, 11 | { 12 | "cell_type": "code", 13 | "execution_count": 2, 14 | "metadata": {}, 15 | "outputs": [ 16 | { 17 | "name": "stdout", 18 | "output_type": "stream", 19 | "text": [ 20 | "Usage: django-clite new [OPTIONS] COMMAND [ARGS]...\n", 21 | "\n", 22 | " Create projects and apps.\n", 23 | "\n", 24 | "Options:\n", 25 | " --help Show this message and exit.\n", 26 | "\n", 27 | "Commands:\n", 28 | " app Creates new django apps.\n", 29 | " project Creates a new django project.\n" 30 | ] 31 | } 32 | ], 33 | "source": [ 34 | "! django-clite new --help" 35 | ] 36 | }, 37 | { 38 | "cell_type": "markdown", 39 | "metadata": {}, 40 | "source": [ 41 | "The `new` command (abbreviated `n`) can be used to start new projects as well as new applications. The command tries to simplify how a project is created as well as the applications contained in it. Here's an example of such simplification:\n", 42 | "\n", 43 | "Suppose you want to start a new project and want to create two apps within it:" 44 | ] 45 | }, 46 | { 47 | "cell_type": "markdown", 48 | "metadata": {}, 49 | "source": [ 50 | "```\n", 51 | "django-admin startproject website && \\\n", 52 | "cd website/website/ && \\\n", 53 | "django-admin startapp blog && \\\n", 54 | "django-admin startapp music\n", 55 | "```" 56 | ] 57 | }, 58 | { 59 | "cell_type": "markdown", 60 | "metadata": {}, 61 | "source": [ 62 | "The equivalent command in the `django-clite` is:" 63 | ] 64 | }, 65 | { 66 | "cell_type": "code", 67 | "execution_count": null, 68 | "metadata": {}, 69 | "outputs": [], 70 | "source": [ 71 | "! django-clite new project website blog music" 72 | ] 73 | }, 74 | { 75 | "cell_type": "markdown", 76 | "metadata": {}, 77 | "source": [ 78 | "Specifying `apps` when creating a project is optional, but you're likely to need to create one inside of your project directory, so the CLI can handle the creation of all of your apps if you pass them as arguments after your project name." 79 | ] 80 | }, 81 | { 82 | "cell_type": "markdown", 83 | "metadata": {}, 84 | "source": [ 85 | "## Creating new projects\n", 86 | "To create a new project, simply run `django-clite new project project_name`. Run `django-clite new project --help` to see which options this command supports." 87 | ] 88 | }, 89 | { 90 | "cell_type": "markdown", 91 | "metadata": {}, 92 | "source": [ 93 | "### Project structure\n", 94 | "This CLI makes some assumptions about the structure of your Django project.\n", 95 | "\n", 96 | "1. It assumes that your apps are one level below the root of your project directory, one level below where `manage.py` is.\n", 97 | "1. It assumes that your app resources are grouped together by type in packages.\n", 98 | "3. Each class representing a `model`, `serializer`, `viewset`, or form is located in its own Python module.\n", 99 | "This is done in order to aid the CLI with the creation and deletion of files\n", 100 | "in the project as we'll see under the [`generate`](#generate) and [`destroy`](#destroy) commands." 101 | ] 102 | }, 103 | { 104 | "cell_type": "markdown", 105 | "metadata": {}, 106 | "source": [ 107 | "## Creating apps" 108 | ] 109 | }, 110 | { 111 | "cell_type": "code", 112 | "execution_count": 7, 113 | "metadata": {}, 114 | "outputs": [ 115 | { 116 | "name": "stdout", 117 | "output_type": "stream", 118 | "text": [ 119 | "Usage: django-clite new apps [OPTIONS] [NAMES]...\n", 120 | "\n", 121 | " Creates new django apps.\n", 122 | "\n", 123 | " This is similar to using `django-admin startapp app_name` but adds more\n", 124 | " structure to your app by creating packages for forms, models, serializers,\n", 125 | " tests, templates, views, and viewsets.\n", 126 | "\n", 127 | " The command can accept multiple apps as arguments. Like so:\n", 128 | "\n", 129 | " django-clite new apps shop blog forum\n", 130 | "\n", 131 | " The command above will create all 4 apps two levels within your project's\n", 132 | " directory, i.e. myproject/myproject. The CLI tries to identify where the\n", 133 | " management module for your project is, so it can place your app files in the\n", 134 | " correct location. This helps with consistency as the CLI can infer\n", 135 | " module/package scopes when performing other automatic configurations.\n", 136 | "\n", 137 | " As part of the CLI convention, each app is assigned its own `urls.py` file,\n", 138 | " which can be used to route urls on a per-app basis. Another convention the\n", 139 | " CLI adopts is to add a viewsets package to the app's directory by default\n", 140 | " (for use with DRF). Within the viewsets directory, a DRF router is\n", 141 | " instantiated in `router.py` and its urls added to each app's urlpatterns by\n", 142 | " default.\n", 143 | "\n", 144 | "Options:\n", 145 | " --is-package Treat as a standalone Python package\n", 146 | " --help Show this message and exit.\n" 147 | ] 148 | } 149 | ], 150 | "source": [ 151 | "! django-clite new apps --help" 152 | ] 153 | } 154 | ], 155 | "metadata": { 156 | "file_extension": ".py", 157 | "kernelspec": { 158 | "display_name": ".venv", 159 | "language": "python", 160 | "name": "python3" 161 | }, 162 | "language_info": { 163 | "codemirror_mode": { 164 | "name": "ipython", 165 | "version": 3 166 | }, 167 | "file_extension": ".py", 168 | "mimetype": "text/x-python", 169 | "name": "python", 170 | "nbconvert_exporter": "python", 171 | "pygments_lexer": "ipython3", 172 | "version": "3.11.5" 173 | }, 174 | "mimetype": "text/x-python", 175 | "name": "python", 176 | "npconvert_exporter": "python", 177 | "orig_nbformat": 2, 178 | "pygments_lexer": "ipython3", 179 | "version": 3 180 | }, 181 | "nbformat": 4, 182 | "nbformat_minor": 2 183 | } 184 | -------------------------------------------------------------------------------- /docs/cli/readme.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# django-clite\n", 8 | "### A CLI tool that handles creating and managing Django projects" 9 | ] 10 | }, 11 | { 12 | "cell_type": "markdown", 13 | "metadata": {}, 14 | "source": [ 15 | "## Installation\n", 16 | "Install via [pip](https://pypi.org/project/django-clite)\n", 17 | "\n", 18 | "```\n", 19 | "pip install django-clite\n", 20 | "```" 21 | ] 22 | }, 23 | { 24 | "cell_type": "markdown", 25 | "metadata": {}, 26 | "source": [ 27 | "Install from source:\n", 28 | "```\n", 29 | "git clone https://bitbucket.org/oleoneto/django-clite.git\n", 30 | "cd django-clite\n", 31 | "pip install .\n", 32 | "```\n", 33 | "\n", 34 | "After installation, the CLI will expose the binary with three names, any of which can be used in place of the another: `D`, `djc`, and `django-clite`. In these documents, we will be using the shorthand, `D`." 35 | ] 36 | }, 37 | { 38 | "cell_type": "markdown", 39 | "metadata": {}, 40 | "source": [ 41 | "## Attributions\n", 42 | "Huge thanks to the folks at [Pallets Projects](https://palletsprojects.com). This CLI tool was built on top of [`click`](https://click.palletsprojects.com) and utilizes [`Jinja`](https://jinja.palletsprojects.com) as the default templating language. Both projects built and maintained by [Pallets](https://palletsprojects.com). \n", 43 | "\n", 44 | "As far as natural language parsing, we utilize [`inflection`](https://pypi.org/project/inflection) for, well, text inflection. In short, we use `inflection` in places were we want to suggest or infer a change in the form of a word. Like, for example, if you specify that your model, say `Ox`, has a foreign key to another model, we will automatically populate the `related_name` attribute of your ForeignKeyField to be the pluralized form of the current model's name. In this case, `related_name='oxen'`. We also use `inflection` in many other places which, if we've done our job well, will be imperceptible to most of you. As of the current version of the CLI, this functionality is only supported in English.\n", 45 | "\n", 46 | "## Motivation\n", 47 | "There's a [Medium post](https://medium.com/@oleoneto/what-ive-learned-from-writing-a-command-line-application-for-django-c08807ca6b67?source=friends_link&sk=79ef1b777c4c7411854010e0dda35052) up about why this CLI exists. Feel free to read it for context about why we think there's a need for it. But, in short, Ruby on Rails, the Django-equivalent web framework for the Ruby community is the motivation for why [`django-clite`](https://pypi.org/project/django-clite) exists. Seriously, go read the [post](https://medium.com/@oleoneto/what-ive-learned-from-writing-a-command-line-application-for-django-c08807ca6b67?source=friends_link&sk=79ef1b777c4c7411854010e0dda35052) for context.\n", 48 | "\n" 49 | ] 50 | }, 51 | { 52 | "cell_type": "markdown", 53 | "metadata": {}, 54 | "source": [ 55 | "## Playground\n", 56 | "Next, we will take a look at how to get things going with the CLI:" 57 | ] 58 | }, 59 | { 60 | "cell_type": "markdown", 61 | "metadata": {}, 62 | "source": [ 63 | "# Commands" 64 | ] 65 | }, 66 | { 67 | "cell_type": "code", 68 | "execution_count": 1, 69 | "metadata": {}, 70 | "outputs": [ 71 | { 72 | "name": "stdout", 73 | "output_type": "stream", 74 | "text": [ 75 | "Usage: django-clite [OPTIONS] COMMAND [ARGS]...\n", 76 | "\n", 77 | " django-clite by Leo Neto\n", 78 | "\n", 79 | " A CLI to handle the creation and management of your Django projects.\n", 80 | "\n", 81 | " The CLI has some opinions about how your project should be structured in\n", 82 | " order for it to maximize the amount of automatic configuration it can\n", 83 | " provide you. Since Django itself is highly configurable, you are free to\n", 84 | " bypass conventions of the CLI if you so choose.\n", 85 | "\n", 86 | "Options:\n", 87 | " --debug Enable debug logs.\n", 88 | " --dry Do not modify the file system.\n", 89 | " -f, --force Override any conflicting files.\n", 90 | " --verbose Enable verbosity.\n", 91 | " --project TEXT Project name.\n", 92 | " --app TEXT Application name.\n", 93 | " --settings TEXT Path to project's settings file.\n", 94 | " --version Show the version and exit.\n", 95 | " --help Show this message and exit.\n", 96 | "\n", 97 | "Commands:\n", 98 | " generate Create application resources.\n", 99 | " new Create projects and apps.\n" 100 | ] 101 | } 102 | ], 103 | "source": [ 104 | "! django-clite --help" 105 | ] 106 | }, 107 | { 108 | "cell_type": "markdown", 109 | "metadata": {}, 110 | "source": [ 111 | "Expore the other commands under the [docs/commands](commands) directory." 112 | ] 113 | } 114 | ], 115 | "metadata": { 116 | "file_extension": ".py", 117 | "kernelspec": { 118 | "display_name": ".venv", 119 | "language": "python", 120 | "name": "python3" 121 | }, 122 | "language_info": { 123 | "codemirror_mode": { 124 | "name": "ipython", 125 | "version": 3 126 | }, 127 | "file_extension": ".py", 128 | "mimetype": "text/x-python", 129 | "name": "python", 130 | "nbconvert_exporter": "python", 131 | "pygments_lexer": "ipython3", 132 | "version": "3.11.5" 133 | }, 134 | "mimetype": "text/x-python", 135 | "name": "python", 136 | "npconvert_exporter": "python", 137 | "orig_nbformat": 2, 138 | "pygments_lexer": "ipython3", 139 | "version": 3 140 | }, 141 | "nbformat": 4, 142 | "nbformat_minor": 2 143 | } 144 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | # import os 14 | # import sys 15 | # sys.path.insert(0, os.path.abspath('.')) 16 | 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = "Django Clite" 21 | copyright = "2019, Leo Neto" 22 | author = "Leo Neto" 23 | 24 | # The full version, including alpha/beta/rc tags 25 | release = "0.18.0" 26 | 27 | 28 | # -- General configuration --------------------------------------------------- 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = [] 34 | 35 | # Add any paths that contain templates here, relative to this directory. 36 | templates_path = ["_templates"] 37 | 38 | # List of patterns, relative to source directory, that match files and 39 | # directories to ignore when looking for source files. 40 | # This pattern also affects html_static_path and html_extra_path. 41 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 42 | 43 | 44 | # -- Options for HTML output ------------------------------------------------- 45 | 46 | # The theme to use for HTML and HTML Help pages. See the documentation for 47 | # a list of builtin themes. 48 | # 49 | html_theme = "alabaster" 50 | 51 | # Add any paths that contain custom static files (such as style sheets) here, 52 | # relative to this directory. They are copied after the builtin static files, 53 | # so a file named "default.css" will overwrite the builtin "default.css". 54 | html_static_path = ["_static"] 55 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. Django Clite documentation master file, created by 2 | sphinx-quickstart on Tue Nov 5 22:31:19 2019. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to Django Clite's documentation! 7 | ======================================== 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | 13 | 14 | 15 | Indices and tables 16 | ================== 17 | 18 | * :ref:`genindex` 19 | * :ref:`modindex` 20 | * :ref:`search` 21 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "django-clite" 3 | description = "CLI for creating and managing Django projects" 4 | authors = [{ name = "Leo Neto", email = "leo@ekletik.com" }] 5 | maintainers = [{ name = "Leo Neto", email = "leo@ekletik.com" }] 6 | requires-python = ">=3.8" 7 | license = { file = "LICENSE" } 8 | readme = "README.md" 9 | version = "0.19.8" 10 | keywords = [ 11 | "django", 12 | "automate", 13 | "cli", 14 | "command line tools", 15 | "rails", 16 | "ember", 17 | "web", 18 | "framework", 19 | ] 20 | classifiers = [ 21 | "Development Status :: 4 - Beta", 22 | "Environment :: Web Environment", 23 | "Framework :: Django :: 4.0", 24 | "Intended Audience :: Developers", 25 | "License :: OSI Approved :: BSD License", 26 | "Natural Language :: English", 27 | "Operating System :: OS Independent", 28 | "Programming Language :: Python", 29 | "Programming Language :: Python :: 3", 30 | "Programming Language :: Python :: 3.6", 31 | "Programming Language :: Python :: 3.7", 32 | "Programming Language :: Python :: 3.8", 33 | "Programming Language :: Python :: 3.9", 34 | "Programming Language :: Python :: 3.10", 35 | "Programming Language :: Python :: 3.11", 36 | "Programming Language :: Python :: 3 :: Only", 37 | "Topic :: Software Development :: Libraries :: Application Frameworks", 38 | "Topic :: Software Development :: Libraries :: Python Modules", 39 | ] 40 | dependencies = [ 41 | "Click>=8.1", 42 | "django>=4.2", 43 | "jinja2>=3.1", 44 | "inquirer>=3.1.3", 45 | "inflection>=0.5.1", 46 | "rich>=13.3", 47 | "geny==0.1.6", 48 | ] 49 | 50 | [project.urls] 51 | Homepage = "https://github.com/oleoneto/django-clite" 52 | Issues = "https://github.com/oleoneto/django-clite/issues" 53 | 54 | [project.scripts] 55 | django-clite = "django_clite.cli:main" 56 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ 3 | django-clite: a cli tool that handles creating and managing Django projects 4 | """ 5 | import os 6 | from django_clite.constants import PLUGINS_ENV_VAR 7 | 8 | __license__ = "BSD 3-Clause" 9 | __author__ = "Leo Neto" 10 | __copyright__ = "Copyright 2019-2023 Leo Neto" 11 | 12 | COMMANDS_FOLDER = os.path.join(os.path.dirname(__file__), "commands") 13 | 14 | PLUGINS_FOLDER = os.environ.get(PLUGINS_ENV_VAR, None) 15 | -------------------------------------------------------------------------------- /src/django_clite/__init__.py: -------------------------------------------------------------------------------- 1 | from .utils import inflect, sanitized_string # noqa 2 | from .core import field_parser, logger, git # noqa 3 | from .commands import callbacks, command_defaults, destroy, generate, new # noqa 4 | from .decorators import scope # noqa 5 | from .extensions import aliased, combined, discoverable # noqa 6 | from .cli import main # noqa 7 | -------------------------------------------------------------------------------- /src/django_clite/cli.py: -------------------------------------------------------------------------------- 1 | import click 2 | import logging 3 | from pathlib import Path 4 | 5 | from rich.logging import RichHandler 6 | 7 | from django_clite.extensions import AliasedAndDiscoverableGroup 8 | from geny.core.filesystem.finder import core_project_files, project_and_app_names 9 | from geny.core.templates.template import TemplateParser 10 | 11 | from django_clite.constants import ( 12 | DJANGO_FILES_KEY, 13 | ENABLE_DRY_RUN_KEY, 14 | ENABLE_DEBUG_KEY, 15 | ENABLE_FORCE_KEY, 16 | ENABLE_VERBOSITY_KEY, 17 | PROJECT_NAME_KEY, 18 | APPLICATION_NAME_KEY, 19 | TEMPLATES_DIRECTORY_ENV_VAR, 20 | ) 21 | 22 | 23 | @click.command( 24 | cls=AliasedAndDiscoverableGroup, context_settings=dict(ignore_unknown_options=True) 25 | ) 26 | @click.option("--debug", is_flag=True, help="Enable debug logs.") 27 | @click.option("--dry", is_flag=True, help="Do not modify the file system.") 28 | @click.option( 29 | "-f", 30 | "--force", 31 | is_flag=True, 32 | envvar=ENABLE_FORCE_KEY, 33 | help="Override any conflicting files.", 34 | ) 35 | @click.option("--verbose", is_flag=True, help="Enable verbosity.") 36 | @click.option("--project", help="Project name.") 37 | @click.option("--app", help="Application name.") 38 | @click.option( 39 | "--templates-dir", 40 | "-t", 41 | envvar=TEMPLATES_DIRECTORY_ENV_VAR, 42 | help="Template directory.", 43 | type=click.Path(), 44 | ) 45 | @click.version_option(package_name="django-clite") 46 | @click.pass_context 47 | def main(ctx, debug, dry, force, verbose, project, app, templates_dir): 48 | """ 49 | django-clite by Leo Neto 50 | 51 | A CLI to handle the creation and management of your Django projects. 52 | 53 | The CLI has some opinions about how your project should be structured in order for it to maximize the 54 | amount of automatic configuration it can provide you. Since Django itself is highly configurable, 55 | you are free to bypass conventions of the CLI if you so choose. 56 | """ 57 | 58 | # Note for contributors: 59 | # 60 | # Commands are auto-discovered if they are placed under the commands directory. 61 | # But please be sure to do the following for this to work: 62 | # 1. Name your package and click command the same. 63 | # 2. Place your command definition within your package's main.py module 64 | # 3. Any sub-commands of your command should be added to the top-most command in the package's main.py module. 65 | # 66 | # Access your command like so: 67 | # `django-clite my-command my-command-sub-command` 68 | # 69 | # If you would like to skip a plugin/command from being auto-discovered, 70 | # simply rename the package by either prepending or appending any number of underscores (_). 71 | # Any code contained within the package will be ignored. 72 | 73 | FORMAT = "[DRY] %(message)s" if dry else "%(message)s" 74 | 75 | logging.basicConfig( 76 | encoding="utf-8", 77 | level=logging.DEBUG if verbose else logging.INFO, 78 | format=FORMAT, 79 | handlers=[ 80 | RichHandler( 81 | log_time_format="", 82 | show_path=False, 83 | show_level=False, 84 | enable_link_path=True, 85 | markup=True, 86 | ) 87 | ], 88 | ) 89 | 90 | django_files = core_project_files() 91 | project_name, app_name = project_and_app_names(django_files) 92 | 93 | templates = [Path(__file__).resolve().parent / "template_files"] 94 | 95 | if templates_dir is not None: 96 | templates.append(Path(templates_dir)) 97 | 98 | TemplateParser( 99 | templates_dir=templates, 100 | context={}, 101 | ) 102 | 103 | btx = { 104 | DJANGO_FILES_KEY: django_files, 105 | ENABLE_DEBUG_KEY: debug, 106 | ENABLE_DRY_RUN_KEY: dry, 107 | ENABLE_FORCE_KEY: force, 108 | ENABLE_VERBOSITY_KEY: verbose, 109 | APPLICATION_NAME_KEY: app or app_name, 110 | } 111 | 112 | # avoids passing an empty project name 113 | if project or project_name: 114 | btx[PROJECT_NAME_KEY] = project or project_name 115 | 116 | ctx.ensure_object(dict) 117 | ctx.obj = btx 118 | 119 | 120 | if __name__ == "__main__": 121 | try: 122 | main() 123 | except (KeyboardInterrupt, SystemExit) as e: 124 | click.echo(f"Exited! {repr(e)}") 125 | -------------------------------------------------------------------------------- /src/django_clite/commands/__init__.py: -------------------------------------------------------------------------------- 1 | # django_clite:commands 2 | -------------------------------------------------------------------------------- /src/django_clite/commands/callbacks.py: -------------------------------------------------------------------------------- 1 | from django_clite.utils import sanitized_string 2 | from django_clite.core.field_parser.factory import AttributeFactory 3 | 4 | 5 | def sanitized_string_callback(_ctx, _param, value): 6 | return sanitized_string(value) 7 | 8 | 9 | def fields_callback(ctx, _param, values): 10 | return AttributeFactory().parsed_fields(values) 11 | -------------------------------------------------------------------------------- /src/django_clite/commands/command_defaults.py: -------------------------------------------------------------------------------- 1 | import inflection 2 | from geny.core.templates.template import TemplateParser 3 | 4 | 5 | def generic(name: str): 6 | return TemplateParser().parse_string( 7 | content="from .{{name}} import {{name}}", 8 | variables={ 9 | "name": name, 10 | "classname": inflection.camelize(name), 11 | }, 12 | ) 13 | 14 | 15 | def admin(name: str): 16 | return TemplateParser().parse_string( 17 | content="from .{{name}} import {{classname}}Admin", 18 | variables={ 19 | "name": name, 20 | "classname": inflection.camelize(name), 21 | }, 22 | ) 23 | 24 | 25 | def admin_inline(name: str): 26 | return TemplateParser().parse_string( 27 | content="from .{{name}} import {{classname}}Inline", 28 | variables={ 29 | "name": name, 30 | "classname": inflection.camelize(name), 31 | }, 32 | ) 33 | 34 | 35 | def form(name: str): 36 | return TemplateParser().parse_string( 37 | content="from .{{name}} import {{classname}}Form", 38 | variables={ 39 | "name": name, 40 | "classname": inflection.camelize(name), 41 | }, 42 | ) 43 | 44 | 45 | def manager(name: str): 46 | return TemplateParser().parse_string( 47 | content="from .{{name}} import {{classname}}Manager", 48 | variables={ 49 | "name": name, 50 | "classname": inflection.camelize(name), 51 | }, 52 | ) 53 | 54 | 55 | def model(name: str): 56 | return TemplateParser().parse_string( 57 | content="from .{{name}} import {{classname}}", 58 | variables={ 59 | "name": name, 60 | "classname": inflection.camelize(name), 61 | }, 62 | ) 63 | 64 | 65 | def serializer(name: str): 66 | return TemplateParser().parse_string( 67 | content="from .{{name}} import {{classname}}Serializer", 68 | variables={ 69 | "name": name, 70 | "classname": inflection.camelize(name), 71 | }, 72 | ) 73 | 74 | 75 | def signal(name: str): 76 | return generic(name) 77 | 78 | 79 | def tag(name: str): 80 | return generic(name) 81 | 82 | 83 | def test(name: str): 84 | return TemplateParser().parse_string( 85 | content="from .{{name}}_test import {{classname}}TestCase", 86 | variables={ 87 | "name": name, 88 | "classname": inflection.camelize(name), 89 | }, 90 | ) 91 | 92 | 93 | def validator(name: str): 94 | return TemplateParser().parse_string( 95 | content="from .{{name}} import {{name}}_validator", 96 | variables={ 97 | "name": name, 98 | "classname": inflection.camelize(name), 99 | }, 100 | ) 101 | 102 | 103 | def view(name: str, klass: str): 104 | return TemplateParser().parse_string( 105 | content="from .{{name}} import {{classname}}", 106 | variables={ 107 | "name": f"{name}{'_' + klass if klass else ''}", 108 | "classname": f"{inflection.camelize(name) + inflection.camelize(klass) + 'View' if klass else name}", 109 | }, 110 | ) 111 | 112 | 113 | def viewset(name: str): 114 | return TemplateParser().parse_string( 115 | content="from .{{name}} import {{classname}}ViewSet", 116 | variables={ 117 | "name": name, 118 | "classname": inflection.camelize(name), 119 | }, 120 | ) 121 | -------------------------------------------------------------------------------- /src/django_clite/commands/destroy/__init__.py: -------------------------------------------------------------------------------- 1 | # django_clite:commands:destroy 2 | -------------------------------------------------------------------------------- /src/django_clite/commands/destroy/admin.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from pathlib import Path 4 | from geny.core.filesystem.files import File 5 | from geny.core.filesystem.transformations import RemoveLineFromFile 6 | from django_clite.decorators.scope import scoped, Scope 7 | from django_clite.commands import command_defaults 8 | from django_clite.commands.callbacks import sanitized_string_callback 9 | 10 | 11 | @scoped(to=Scope.APP) 12 | @click.command() 13 | @click.argument("name", callback=sanitized_string_callback) 14 | @click.pass_context 15 | def admin(ctx, name): 16 | """ 17 | Destroy an admin model. 18 | """ 19 | 20 | File(name=f"admin/{name}.py").destroy( 21 | after_hooks=[ 22 | RemoveLineFromFile(Path("admin/__init__.py"), command_defaults.admin(name)), 23 | ], 24 | **ctx.obj, 25 | ) 26 | 27 | 28 | @scoped(to=Scope.APP) 29 | @click.command() 30 | @click.argument("name", callback=sanitized_string_callback) 31 | @click.pass_context 32 | def admin_inline(ctx, name): 33 | """ 34 | Destroy an inline admin model. 35 | """ 36 | 37 | File(name=f"admin/inlines/{name}.py").destroy( 38 | after_hooks=[ 39 | RemoveLineFromFile( 40 | Path("admin/inlines/__init__.py"), command_defaults.admin_inline(name) 41 | ), 42 | ], 43 | **ctx.obj, 44 | ) 45 | -------------------------------------------------------------------------------- /src/django_clite/commands/destroy/dockerfile.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from geny.core.filesystem.files import File 4 | from django_clite.decorators.scope import scoped, Scope 5 | 6 | 7 | @scoped(to=Scope.PROJECT) 8 | @click.command() 9 | @click.option("--compose", is_flag=True) 10 | @click.pass_context 11 | def dockerfile(ctx, compose): 12 | """ 13 | Destroy a Dockerfile (and docker-compose.yaml). 14 | """ 15 | 16 | files = [ 17 | File(name="Dockerfile"), 18 | ] 19 | 20 | if compose: 21 | files.append(File(name="docker-compose.yaml")) 22 | 23 | [ 24 | file.destroy( 25 | **ctx.obj, 26 | ) 27 | for file in files 28 | ] 29 | -------------------------------------------------------------------------------- /src/django_clite/commands/destroy/fixtures.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from geny.core.filesystem.files import File 4 | from django_clite.commands.callbacks import sanitized_string_callback 5 | from django_clite.decorators.scope import scoped, Scope 6 | 7 | 8 | @scoped(to=Scope.APP) 9 | @click.command() 10 | @click.argument("model", required=True, callback=sanitized_string_callback) 11 | @click.pass_context 12 | def fixture(ctx, model): 13 | """ 14 | Destroy model fixtures. 15 | """ 16 | 17 | File(name=f"fixtures/{model}.json").destroy(**ctx.obj) 18 | -------------------------------------------------------------------------------- /src/django_clite/commands/destroy/forms.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from pathlib import Path 4 | from geny.core.filesystem.files import File 5 | from geny.core.filesystem.transformations import RemoveLineFromFile 6 | from django_clite.decorators.scope import scoped, Scope 7 | from django_clite.commands.callbacks import sanitized_string_callback 8 | from django_clite.commands import command_defaults 9 | 10 | 11 | @scoped(to=Scope.APP) 12 | @click.command() 13 | @click.argument("name", required=True, callback=sanitized_string_callback) 14 | @click.pass_context 15 | def form(ctx, name): 16 | """ 17 | Destroy a form. 18 | """ 19 | 20 | File(name=f"forms/{name}.py").destroy( 21 | after_hooks=[ 22 | RemoveLineFromFile(Path("forms/__init__.py"), command_defaults.form(name)), 23 | ], 24 | **ctx.obj, 25 | ) 26 | -------------------------------------------------------------------------------- /src/django_clite/commands/destroy/main.py: -------------------------------------------------------------------------------- 1 | # django_clite:commands:destroy 2 | import click 3 | 4 | from django_clite.commands.destroy.admin import admin, admin_inline as admin_inline 5 | from django_clite.commands.destroy.dockerfile import dockerfile 6 | from django_clite.commands.destroy.fixtures import fixture 7 | from django_clite.commands.destroy.forms import form 8 | from django_clite.commands.destroy.management import management 9 | from django_clite.commands.destroy.managers import manager 10 | from django_clite.commands.destroy.models import model, scaffold 11 | from django_clite.commands.destroy.serializers import serializer 12 | from django_clite.commands.destroy.signals import signal 13 | from django_clite.commands.destroy.tags import tag 14 | from django_clite.commands.destroy.template import template 15 | from django_clite.commands.destroy.tests import test 16 | from django_clite.commands.destroy.validators import validator 17 | from django_clite.commands.destroy.views import view 18 | from django_clite.commands.destroy.viewsets import viewset 19 | 20 | 21 | @click.group() 22 | @click.pass_context 23 | def destroy(ctx): 24 | """ 25 | Destroy application resources. 26 | """ 27 | 28 | ctx.ensure_object(dict) 29 | 30 | 31 | [ 32 | destroy.add_command(cmd) 33 | for cmd in [ 34 | admin, 35 | admin_inline, 36 | dockerfile, 37 | fixture, 38 | form, 39 | management, 40 | manager, 41 | model, 42 | scaffold, 43 | serializer, 44 | signal, 45 | tag, 46 | template, 47 | test, 48 | validator, 49 | view, 50 | viewset, 51 | ] 52 | ] 53 | -------------------------------------------------------------------------------- /src/django_clite/commands/destroy/management.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from pathlib import Path 4 | from geny.core.filesystem.files import File 5 | from geny.core.filesystem.transformations import RemoveLineFromFile 6 | from django_clite.decorators.scope import scoped, Scope 7 | from django_clite.commands.callbacks import sanitized_string_callback 8 | from django_clite.commands import command_defaults 9 | 10 | 11 | @scoped(to=Scope.APP) 12 | @click.command(name="command") 13 | @click.argument("name", required=True, callback=sanitized_string_callback) 14 | @click.pass_context 15 | def management(ctx, name): 16 | """ 17 | Destroy an application command. 18 | """ 19 | 20 | File(name=f"management/{name}.py").destroy(**ctx.obj) 21 | 22 | File(name=f"admin/inlines/{name}.py").destroy( 23 | after_hooks=[ 24 | RemoveLineFromFile( 25 | Path("admin/inlines/__init__.py"), command_defaults.admin(name) 26 | ), 27 | ], 28 | **ctx.obj, 29 | ) 30 | -------------------------------------------------------------------------------- /src/django_clite/commands/destroy/managers.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from pathlib import Path 4 | from geny.core.filesystem.files import File 5 | from geny.core.filesystem.transformations import RemoveLineFromFile 6 | from django_clite.decorators.scope import scoped, Scope 7 | from django_clite.commands.callbacks import sanitized_string_callback 8 | from django_clite.commands import command_defaults 9 | 10 | 11 | @scoped(to=Scope.APP) 12 | @click.command() 13 | @click.argument("name", required=True, callback=sanitized_string_callback) 14 | @click.pass_context 15 | def manager(ctx, name): 16 | """ 17 | Destroy a model manager. 18 | """ 19 | 20 | File(name=f"models/managers/{name}.py").destroy( 21 | after_hooks=[ 22 | RemoveLineFromFile( 23 | Path("models/managers/__init__.py"), command_defaults.manager(name) 24 | ), 25 | ], 26 | **ctx.obj, 27 | ) 28 | -------------------------------------------------------------------------------- /src/django_clite/commands/destroy/models.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from pathlib import Path 4 | from geny.core.filesystem.files import File 5 | from geny.core.filesystem.transformations import RemoveLineFromFile 6 | from django_clite.core.logger import logger 7 | from django_clite.decorators.scope import scoped, Scope 8 | from django_clite.commands import command_defaults 9 | from django_clite.commands.callbacks import sanitized_string_callback 10 | 11 | 12 | @scoped(to=Scope.APP) 13 | @click.command() 14 | @click.argument("name", required=True, callback=sanitized_string_callback) 15 | @click.option("--api", is_flag=True, help="Destroy only related api resources") 16 | @click.option("--full", is_flag=True, help="Destroy all related resources") 17 | @click.option("--admin", is_flag=True, help="Destroy admin model") 18 | @click.option("--fixtures", is_flag=True, help="Destroy model fixture") 19 | @click.option("--form", is_flag=True, help="Destroy model form") 20 | @click.option("--serializers", is_flag=True, help="Destroy serializers") 21 | @click.option("--templates", is_flag=True, help="Destroy templates") 22 | @click.option("--tests", is_flag=True, help="Destroy tests") 23 | @click.option("--views", is_flag=True, help="Destroy views") 24 | @click.option("--viewsets", is_flag=True, help="Destroy viewsets") 25 | @click.pass_context 26 | def model( 27 | ctx, 28 | name, 29 | api, 30 | full, 31 | admin, 32 | fixtures, 33 | form, 34 | serializers, 35 | templates, 36 | tests, 37 | views, 38 | viewsets, 39 | ): 40 | """ 41 | Destroy a model. 42 | """ 43 | 44 | if api and full: 45 | logger.error("Flags --api and --full cannot be used simultaneously.") 46 | raise click.Abort() 47 | 48 | File(name=f"models/{name}.py").destroy( 49 | after_hooks=[ 50 | RemoveLineFromFile( 51 | Path("models/__init__.py"), command_defaults.model(name) 52 | ), 53 | ], 54 | **ctx.obj, 55 | ) 56 | 57 | def destroy_related_resources(): 58 | if admin or api or full: 59 | from .admin import admin as cmd 60 | 61 | ctx.invoke(cmd, name=name) 62 | 63 | if fixtures or api or full: 64 | from .fixtures import fixture as cmd 65 | 66 | ctx.invoke(cmd, model=name) 67 | 68 | if form or full: 69 | from .forms import form as cmd 70 | 71 | ctx.invoke(cmd, name=name) 72 | 73 | if serializers or api or full: 74 | from .serializers import serializer as cmd 75 | 76 | ctx.invoke(cmd, name=name) 77 | 78 | if templates or api or full: 79 | from .template import template as cmd 80 | 81 | ctx.invoke(cmd, name=name, full=full) 82 | 83 | if tests or api or full: 84 | from .tests import test as cmd 85 | 86 | ctx.invoke(cmd, name=name, full=full) 87 | 88 | if views or full: 89 | from .views import view as cmd 90 | 91 | ctx.invoke(cmd, name=name, full=full) 92 | 93 | if viewsets or api or full: 94 | from .viewsets import viewset as cmd 95 | 96 | ctx.invoke(cmd, name=name) 97 | 98 | destroy_related_resources() 99 | 100 | 101 | @scoped(to=Scope.APP) 102 | @click.command() 103 | @click.argument("name", required=True, callback=sanitized_string_callback) 104 | @click.pass_context 105 | def scaffold(ctx, name): 106 | """ 107 | Delete all resources for a given model. 108 | """ 109 | 110 | ctx.invoke(model, name=name, full=True) 111 | -------------------------------------------------------------------------------- /src/django_clite/commands/destroy/serializers.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from pathlib import Path 4 | from geny.core.filesystem.files import File 5 | from geny.core.filesystem.transformations import RemoveLineFromFile 6 | from django_clite.decorators.scope import scoped, Scope 7 | from django_clite.commands import command_defaults 8 | from django_clite.commands.callbacks import sanitized_string_callback 9 | 10 | 11 | @scoped(to=Scope.APP) 12 | @click.command() 13 | @click.argument("name", required=True, callback=sanitized_string_callback) 14 | @click.pass_context 15 | def serializer(ctx, name): 16 | """ 17 | Destroy a serializer for a given model. 18 | """ 19 | 20 | File(name=f"serializers/{name}.py").destroy( 21 | after_hooks=[ 22 | RemoveLineFromFile( 23 | Path("serializers/__init__.py"), command_defaults.serializer(name) 24 | ), 25 | ], 26 | **ctx.obj, 27 | ) 28 | -------------------------------------------------------------------------------- /src/django_clite/commands/destroy/signals.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from pathlib import Path 4 | from geny.core.filesystem.files import File 5 | from geny.core.filesystem.transformations import RemoveLineFromFile 6 | from django_clite.decorators.scope import scoped, Scope 7 | from django_clite.commands import command_defaults 8 | from django_clite.commands.callbacks import sanitized_string_callback 9 | 10 | 11 | @scoped(to=Scope.APP) 12 | @click.command() 13 | @click.argument("name", required=True, callback=sanitized_string_callback) 14 | @click.pass_context 15 | def signal(ctx, name): 16 | """ 17 | Destroy a signal. 18 | """ 19 | 20 | File(name=f"models/signals/{name}.py").destroy( 21 | after_hooks=[ 22 | RemoveLineFromFile( 23 | Path("models/signals/__init__.py"), command_defaults.signal(name) 24 | ), 25 | ], 26 | **ctx.obj, 27 | ) 28 | -------------------------------------------------------------------------------- /src/django_clite/commands/destroy/tags.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from pathlib import Path 4 | from geny.core.filesystem.files import File 5 | from geny.core.filesystem.transformations import RemoveLineFromFile 6 | from django_clite.decorators.scope import scoped, Scope 7 | from django_clite.commands import command_defaults 8 | from django_clite.commands.callbacks import sanitized_string_callback 9 | 10 | 11 | @scoped(to=Scope.APP) 12 | @click.command() 13 | @click.argument("name", required=True, callback=sanitized_string_callback) 14 | @click.pass_context 15 | def tag(ctx, name): 16 | """ 17 | Generate a template tag. 18 | """ 19 | 20 | File(name=f"templatetags/{name}.py").destroy( 21 | after_hooks=[ 22 | RemoveLineFromFile( 23 | Path("templatetags/__init__.py"), command_defaults.tag(name) 24 | ), 25 | ], 26 | **ctx.obj, 27 | ) 28 | -------------------------------------------------------------------------------- /src/django_clite/commands/destroy/template.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from geny.core.filesystem.files import File 4 | from django_clite.decorators.scope import scoped, Scope 5 | from django_clite.commands.callbacks import sanitized_string_callback 6 | 7 | 8 | SUPPORTED_CLASSES = [ 9 | "create", 10 | "detail", 11 | "list", 12 | "update", 13 | ] 14 | 15 | 16 | @scoped(to=Scope.APP) 17 | @click.command(context_settings=dict(ignore_unknown_options=True)) 18 | @click.argument("name", required=True, callback=sanitized_string_callback) 19 | @click.option("--class_", type=click.Choice(SUPPORTED_CLASSES)) 20 | @click.option("--full", is_flag=True, help="Destroy templates for all CRUD operations") 21 | @click.pass_context 22 | def template(ctx, name, class_, full): 23 | """ 24 | Destroy an html template. 25 | """ 26 | 27 | classes = SUPPORTED_CLASSES if full else [class_] 28 | 29 | for k in classes: 30 | File(name=f"templates/{name}{'_' + k if k else ''}.html").destroy(**ctx.obj) 31 | -------------------------------------------------------------------------------- /src/django_clite/commands/destroy/tests.py: -------------------------------------------------------------------------------- 1 | import click 2 | import inflection 3 | 4 | from pathlib import Path 5 | from geny.core.filesystem.files import File 6 | from geny.core.filesystem.transformations import RemoveLineFromFile 7 | from django_clite.core.logger import logger 8 | from django_clite.decorators.scope import scoped, Scope 9 | from django_clite.commands import command_defaults 10 | from django_clite.commands.callbacks import sanitized_string_callback 11 | 12 | 13 | SUPPORTED_SCOPES = [ 14 | "model", 15 | "viewset", 16 | ] 17 | 18 | 19 | @scoped(to=Scope.APP) 20 | @click.command() 21 | @click.argument("name", required=True, callback=sanitized_string_callback) 22 | @click.option("--scope", required=True, type=click.Choice(SUPPORTED_SCOPES)) 23 | @click.option("--full", is_flag=True, help=f"Destroy tests for {SUPPORTED_SCOPES}") 24 | @click.pass_context 25 | def test(ctx, name, scope, full): 26 | """ 27 | Destroy TestCases. 28 | """ 29 | 30 | if scope and full: 31 | logger.error("Flags --scope and --full cannot be used simultaneously.") 32 | raise click.Abort() 33 | 34 | scopes = SUPPORTED_SCOPES if full else [scope] 35 | 36 | for s in scopes: 37 | file = File(name=f"tests/{inflection.pluralize(s)}/{name}_test.py") 38 | 39 | file.destroy( 40 | after_hooks=[ 41 | RemoveLineFromFile( 42 | Path(f"tests/{inflection.pluralize(s)}/__init__.py"), 43 | command_defaults.test(name), 44 | ), 45 | ], 46 | **ctx.obj, 47 | ) 48 | -------------------------------------------------------------------------------- /src/django_clite/commands/destroy/validators.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from pathlib import Path 4 | from geny.core.filesystem.files import File 5 | from geny.core.filesystem.transformations import RemoveLineFromFile 6 | from django_clite.decorators.scope import scoped, Scope 7 | from django_clite.commands import command_defaults 8 | from django_clite.commands.callbacks import sanitized_string_callback 9 | 10 | 11 | @scoped(to=Scope.APP) 12 | @click.command() 13 | @click.argument("name", required=True, callback=sanitized_string_callback) 14 | @click.pass_context 15 | def validator(ctx, name): 16 | """ 17 | Destroy a validator. 18 | """ 19 | 20 | File(name=f"models/validators/{name}.py").destroy( 21 | after_hooks=[ 22 | RemoveLineFromFile( 23 | Path("models/validators/__init__.py"), command_defaults.validator(name) 24 | ), 25 | ], 26 | **ctx.obj, 27 | ) 28 | -------------------------------------------------------------------------------- /src/django_clite/commands/destroy/views.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from pathlib import Path 4 | from geny.core.filesystem.files import File 5 | from geny.core.filesystem.transformations import RemoveLineFromFile 6 | from django_clite.decorators.scope import scoped, Scope 7 | from .. import command_defaults 8 | from django_clite.commands.callbacks import sanitized_string_callback 9 | from .template import template 10 | 11 | 12 | SUPPORTED_CLASSES = [ 13 | "create", 14 | "detail", 15 | "list", 16 | "update", 17 | ] 18 | 19 | 20 | @scoped(to=Scope.APP) 21 | @click.command() 22 | @click.argument("name", required=True, callback=sanitized_string_callback) 23 | @click.option("--class_", type=click.Choice(SUPPORTED_CLASSES)) 24 | @click.option("--full", is_flag=True, help="Delete all CRUD views") 25 | @click.option( 26 | "--keep-templates", 27 | is_flag=True, 28 | default=False, 29 | help="Destroy related templates.", 30 | ) 31 | @click.pass_context 32 | def view(ctx, name, class_, full, keep_templates): 33 | """ 34 | Destroy a view function or class. 35 | """ 36 | 37 | classes = SUPPORTED_CLASSES if full else [class_] 38 | 39 | for k in classes: 40 | File(name=f"views/{name}{'_' + k if k else ''}.py").destroy( 41 | after_hooks=[ 42 | RemoveLineFromFile( 43 | Path("views/__init__.py"), 44 | command_defaults.view(name, klass=k), 45 | ), 46 | ], 47 | **ctx.obj, 48 | ) 49 | 50 | if keep_templates: 51 | return 52 | 53 | for class_ in classes: 54 | ctx.invoke(template, name=name, class_=class_) 55 | -------------------------------------------------------------------------------- /src/django_clite/commands/destroy/viewsets.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from pathlib import Path 4 | from geny.core.filesystem.files import File 5 | from geny.core.filesystem.transformations import RemoveLineFromFile 6 | from django_clite.decorators.scope import scoped, Scope 7 | from django_clite.commands import command_defaults 8 | from django_clite.commands.callbacks import sanitized_string_callback 9 | 10 | 11 | @scoped(to=Scope.APP) 12 | @click.command() 13 | @click.argument("name", required=True, callback=sanitized_string_callback) 14 | @click.option("--full", is_flag=True, help="Destroy related files (i.e. TestCases)") 15 | @click.pass_context 16 | def viewset(ctx, name, full): 17 | """ 18 | Destroy a viewset for a serializable model. 19 | """ 20 | 21 | File(name=f"viewsets/{name}.py").destroy( 22 | after_hooks=[ 23 | RemoveLineFromFile( 24 | Path("viewsets/__init__.py"), command_defaults.viewset(name) 25 | ), 26 | ], 27 | **ctx.obj, 28 | ) 29 | 30 | if full: 31 | from .tests import test as cmd 32 | 33 | ctx.invoke(cmd, name=name, scope="viewset") 34 | -------------------------------------------------------------------------------- /src/django_clite/commands/generate/__init__.py: -------------------------------------------------------------------------------- 1 | # django_clite:commands:generate 2 | -------------------------------------------------------------------------------- /src/django_clite/commands/generate/admin.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | 3 | import click 4 | import inflection 5 | 6 | from geny.core.filesystem.files import File 7 | from geny.core.filesystem.transformations import AddLineToFile, TouchFile 8 | from django_clite.commands import command_defaults 9 | from django_clite.commands.callbacks import sanitized_string_callback, fields_callback 10 | from django_clite.decorators.scope import scoped, Scope 11 | 12 | 13 | @scoped(to=Scope.APP) 14 | @click.command() 15 | @click.argument("name", callback=sanitized_string_callback) 16 | @click.argument("fields", nargs=-1, required=False, callback=fields_callback) 17 | @click.option( 18 | "--permissions", is_flag=True, help="Add permission stubs to admin model." 19 | ) 20 | @click.option( 21 | "--skip-import", 22 | is_flag=True, 23 | default=False, 24 | help="Do not import in __init__ module", 25 | ) 26 | @click.pass_context 27 | def admin(ctx, name, fields, permissions, skip_import): 28 | """ 29 | Generate an admin model. 30 | """ 31 | 32 | admin_fields = [attr_name for attr_name, v in fields.items() if v.supports_admin] 33 | 34 | file = File( 35 | name=f"admin/{name}.py", 36 | template="admin/admin.tpl", 37 | context={ 38 | "classname": inflection.camelize(name), 39 | "fields": admin_fields, 40 | "name": name, 41 | "permissions": permissions, 42 | }, 43 | ) 44 | 45 | after_hooks = [TouchFile("admin/__init__.py")] 46 | 47 | if not skip_import: 48 | after_hooks.append( 49 | AddLineToFile( 50 | pathlib.Path("admin/__init__.py"), 51 | command_defaults.admin(name), 52 | prevent_duplicates=True, 53 | ) 54 | ) 55 | 56 | file.create(after_hooks=after_hooks, **ctx.obj) 57 | 58 | 59 | @scoped(to=Scope.APP) 60 | @click.command() 61 | @click.argument("name", callback=sanitized_string_callback) 62 | @click.option( 63 | "--skip-import", 64 | is_flag=True, 65 | default=False, 66 | help="Do not import in __init__ module", 67 | ) 68 | @click.pass_context 69 | def admin_inline(ctx, name, skip_import): 70 | """ 71 | Generate an inline admin model. 72 | """ 73 | 74 | file = File( 75 | name=f"admin/inlines/{name}.py", 76 | template="admin/inline.tpl", 77 | context={ 78 | "classname": inflection.camelize(name), 79 | "name": name, 80 | }, 81 | ) 82 | 83 | after_hooks = [TouchFile("admin/inlines/__init__.py")] 84 | 85 | if not skip_import: 86 | after_hooks.append( 87 | AddLineToFile( 88 | pathlib.Path("admin/inlines/__init__.py"), 89 | command_defaults.admin_inline(name), 90 | prevent_duplicates=True, 91 | ) 92 | ) 93 | 94 | file.create(after_hooks=after_hooks, **ctx.obj) 95 | -------------------------------------------------------------------------------- /src/django_clite/commands/generate/dockerfile.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from geny.core.filesystem.files import File 4 | from django_clite.decorators.scope import scoped, Scope 5 | 6 | 7 | @scoped(to=Scope.PROJECT) 8 | @click.command() 9 | @click.option("--compose", is_flag=True) 10 | @click.pass_context 11 | def dockerfile(ctx, compose): 12 | """ 13 | Generate a Dockerfile. 14 | """ 15 | 16 | files = [ 17 | File(name="Dockerfile", template="docker/dockerfile.tpl", context={}), 18 | ] 19 | 20 | if compose: 21 | files.append( 22 | File( 23 | name="docker-compose.yaml", 24 | template="docker/docker-compose.tpl", 25 | context={ 26 | "services": [ 27 | "database", 28 | "redis", 29 | "celery", 30 | ], 31 | }, 32 | ) 33 | ) 34 | 35 | [ 36 | file.create( 37 | **ctx.obj, 38 | ) 39 | for file in files 40 | ] 41 | -------------------------------------------------------------------------------- /src/django_clite/commands/generate/fixtures.py: -------------------------------------------------------------------------------- 1 | import click 2 | import inflection 3 | 4 | from geny.core.filesystem.files import File 5 | from django_clite.decorators.scope import scoped, Scope 6 | from django_clite.commands.callbacks import sanitized_string_callback, fields_callback 7 | 8 | 9 | @scoped(to=Scope.APP) 10 | @click.command() 11 | @click.argument("model", required=True, callback=sanitized_string_callback) 12 | @click.option("--total", default=1, help="Number of fixtures to be created.") 13 | @click.argument("fields", nargs=-1, required=False, callback=fields_callback) 14 | @click.pass_context 15 | def fixture(ctx, model, total, fields): 16 | """ 17 | Generate model fixtures. 18 | """ 19 | 20 | file = File( 21 | name=f"fixtures/{model}.json", 22 | template="fixture.tpl", 23 | context={ 24 | "total": total, 25 | "fields": fields, 26 | "classname": inflection.camelize(model), 27 | }, 28 | ) 29 | 30 | file.create(**ctx.obj) 31 | -------------------------------------------------------------------------------- /src/django_clite/commands/generate/forms.py: -------------------------------------------------------------------------------- 1 | import click 2 | import pathlib 3 | import inflection 4 | 5 | from geny.core.filesystem.files import File 6 | from geny.core.filesystem.transformations import AddLineToFile, TouchFile 7 | from django_clite.decorators.scope import scoped, Scope 8 | from django_clite.commands import command_defaults 9 | from django_clite.commands.callbacks import sanitized_string_callback 10 | 11 | 12 | @scoped(to=Scope.APP) 13 | @click.command() 14 | @click.argument("name", required=True, callback=sanitized_string_callback) 15 | @click.option( 16 | "--skip-import", 17 | is_flag=True, 18 | default=False, 19 | help="Do not import in __init__ module", 20 | ) 21 | @click.pass_context 22 | def form(ctx, name, skip_import): 23 | """ 24 | Generate a form. 25 | """ 26 | 27 | file = File( 28 | name=f"forms/{name}.py", 29 | template="form.tpl", 30 | context={ 31 | "name": name, 32 | "classname": inflection.camelize(name), 33 | }, 34 | ) 35 | 36 | after_hooks = [TouchFile("forms/__init__.py")] 37 | 38 | if not skip_import: 39 | after_hooks.append( 40 | AddLineToFile( 41 | pathlib.Path("forms/__init__.py"), 42 | command_defaults.form(name), 43 | prevent_duplicates=True, 44 | ) 45 | ) 46 | 47 | file.create(after_hooks=after_hooks, **ctx.obj) 48 | -------------------------------------------------------------------------------- /src/django_clite/commands/generate/main.py: -------------------------------------------------------------------------------- 1 | # django_clite:commands:generate 2 | import click 3 | 4 | from django_clite.commands.generate.admin import admin, admin_inline as admin_inline 5 | from django_clite.commands.generate.dockerfile import dockerfile 6 | from django_clite.commands.generate.fixtures import fixture 7 | from django_clite.commands.generate.forms import form 8 | from django_clite.commands.generate.management import management 9 | from django_clite.commands.generate.managers import manager 10 | from django_clite.commands.generate.models import model, scaffold 11 | from django_clite.commands.generate.serializers import serializer 12 | from django_clite.commands.generate.signals import signal 13 | from django_clite.commands.generate.tags import tag 14 | from django_clite.commands.generate.template import template 15 | from django_clite.commands.generate.tests import test 16 | from django_clite.commands.generate.validators import validator 17 | from django_clite.commands.generate.views import view 18 | from django_clite.commands.generate.viewsets import viewset 19 | 20 | 21 | @click.group() 22 | @click.option("--project", type=click.Path(), help="Project name.") 23 | @click.option("--app", type=click.Path(), help="Application name.") 24 | @click.pass_context 25 | def generate(ctx, project, app): 26 | """ 27 | Create application resources. 28 | """ 29 | 30 | ctx.ensure_object(dict) 31 | 32 | 33 | [ 34 | generate.add_command(cmd) 35 | for cmd in [ 36 | admin, 37 | admin_inline, 38 | dockerfile, 39 | fixture, 40 | form, 41 | management, 42 | manager, 43 | model, 44 | scaffold, 45 | serializer, 46 | signal, 47 | tag, 48 | template, 49 | test, 50 | validator, 51 | view, 52 | viewset, 53 | ] 54 | ] 55 | -------------------------------------------------------------------------------- /src/django_clite/commands/generate/management.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from geny.core.filesystem.files import File 4 | from django_clite.decorators.scope import scoped, Scope 5 | from django_clite.commands.callbacks import sanitized_string_callback 6 | 7 | 8 | @scoped(to=Scope.APP) 9 | @click.command(name="command") 10 | @click.argument("name", required=True, callback=sanitized_string_callback) 11 | @click.pass_context 12 | def management(ctx, name): 13 | """ 14 | Generate an application command. 15 | """ 16 | 17 | file = File( 18 | name=f"management/{name}.py", 19 | template="management.tpl", 20 | context={ 21 | "name": name, 22 | }, 23 | ) 24 | 25 | file.create(**ctx.obj) 26 | -------------------------------------------------------------------------------- /src/django_clite/commands/generate/managers.py: -------------------------------------------------------------------------------- 1 | import click 2 | import pathlib 3 | import inflection 4 | 5 | from geny.core.filesystem.files import File 6 | from geny.core.filesystem.transformations import AddLineToFile, TouchFile 7 | from django_clite.decorators.scope import scoped, Scope 8 | from django_clite.commands import command_defaults 9 | from django_clite.commands.callbacks import sanitized_string_callback 10 | 11 | 12 | @scoped(to=Scope.APP) 13 | @click.command() 14 | @click.argument("name", required=True, callback=sanitized_string_callback) 15 | @click.option( 16 | "--skip-import", 17 | is_flag=True, 18 | default=False, 19 | help="Do not import in __init__ module", 20 | ) 21 | @click.pass_context 22 | def manager(ctx, name, skip_import): 23 | """ 24 | Generate a model manager. 25 | """ 26 | 27 | file = File( 28 | name=f"models/managers/{name}.py", 29 | template="models/manager.tpl", 30 | context={ 31 | "name": name, 32 | "classname": inflection.camelize(name), 33 | }, 34 | ) 35 | 36 | after_hooks = [TouchFile("models/managers/__init__.py")] 37 | 38 | if not skip_import: 39 | after_hooks.append( 40 | AddLineToFile( 41 | pathlib.Path("models/managers/__init__.py"), 42 | command_defaults.manager(name), 43 | prevent_duplicates=True, 44 | ) 45 | ) 46 | 47 | file.create(after_hooks=after_hooks, **ctx.obj) 48 | -------------------------------------------------------------------------------- /src/django_clite/commands/generate/models.py: -------------------------------------------------------------------------------- 1 | import click 2 | import pathlib 3 | import inflection 4 | 5 | from geny.core.filesystem.files import File 6 | from geny.core.filesystem.transformations import AddLineToFile, TouchFile 7 | 8 | from django_clite.core.logger import logger 9 | from django_clite.decorators.scope import scoped, Scope 10 | from django_clite.commands import command_defaults 11 | from django_clite.commands.callbacks import sanitized_string_callback, fields_callback 12 | from django_clite.constants import APPLICATION_NAME_KEY 13 | 14 | 15 | @scoped(to=Scope.APP) 16 | @click.command() 17 | @click.argument("name", required=True, callback=sanitized_string_callback) 18 | @click.argument("fields", nargs=-1, required=False, callback=fields_callback) 19 | @click.option("-a", "--abstract", is_flag=True, help="Creates an abstract model type") 20 | @click.option("--api", is_flag=True, help="Adds only related api resources") 21 | @click.option("--full", is_flag=True, help="Adds all related resources") 22 | @click.option("--admin", is_flag=True, help="Register admin model") 23 | @click.option("--fixtures", is_flag=True, help="Create model fixture") 24 | @click.option("--form", is_flag=True, help="Create model form") 25 | @click.option("--serializers", is_flag=True, help="Create serializers") 26 | # @click.option("--templates", is_flag=True, help="Create templates") 27 | @click.option("--tests", is_flag=True, help="Create tests") 28 | @click.option("--views", is_flag=True, help="Create views") 29 | @click.option("--viewsets", is_flag=True, help="Create viewsets") 30 | @click.option( 31 | "--skip-import", 32 | is_flag=True, 33 | default=False, 34 | help="Do not import in __init__ module", 35 | ) 36 | @click.pass_context 37 | def model( 38 | ctx, 39 | name, 40 | fields, 41 | abstract, 42 | api, 43 | full, 44 | admin, 45 | fixtures, 46 | form, 47 | serializers, 48 | # templates, 49 | tests, 50 | views, 51 | viewsets, 52 | skip_import, 53 | ): 54 | """ 55 | Generates a model under the models directory. 56 | One can specify multiple attributes after the model's name, like so: 57 | 58 | django-clite g model track int:number char:title fk:album bool:is_favorite 59 | 60 | This will generate a Track model and add a foreign key of Album. 61 | If the model is to be added to admin.site one can optionally opt in by specifying the --register-admin flag. 62 | """ 63 | 64 | if api and full: 65 | logger.error("Flags --api and --full cannot be used simultaneously.") 66 | raise click.Abort() 67 | 68 | current_app_scope = ctx.obj.get(APPLICATION_NAME_KEY) 69 | 70 | file = File( 71 | name=f"models/{name}.py", 72 | template="models/model.tpl", 73 | context={ 74 | "api": api, 75 | "abstract": abstract, 76 | "classname": inflection.camelize(name), 77 | "fields": fields, 78 | "imports": dict((k, v) for k, v in fields.items() if v.is_relationship), 79 | "name": name, 80 | "table_name": f"{current_app_scope}.{inflection.pluralize(name)}", 81 | }, 82 | ) 83 | 84 | after_hooks = [TouchFile("models/__init__.py")] 85 | 86 | if not skip_import: 87 | after_hooks.append( 88 | AddLineToFile( 89 | pathlib.Path("models/__init__.py"), 90 | command_defaults.model(name), 91 | prevent_duplicates=True, 92 | ) 93 | ) 94 | 95 | file.create(after_hooks=after_hooks, **ctx.obj) 96 | 97 | def generate_related_resources(): 98 | if admin or api or full: 99 | from .admin import admin as cmd 100 | 101 | ctx.invoke(cmd, name=name, fields=fields, skip_import=skip_import) 102 | 103 | if fixtures or api or full: 104 | from .fixtures import fixture as cmd 105 | 106 | ctx.invoke(cmd, model=name, fields=fields) 107 | 108 | if form or full: 109 | from .forms import form as cmd 110 | 111 | ctx.invoke(cmd, name=name, skip_import=skip_import) 112 | 113 | if serializers or api or full: 114 | from .serializers import serializer as cmd 115 | 116 | ctx.invoke(cmd, name=name, skip_import=skip_import) 117 | 118 | if tests or api or full: 119 | from .tests import test as cmd 120 | 121 | ctx.invoke(cmd, name=name, full=full) 122 | 123 | if views or full: 124 | from .views import view as cmd 125 | 126 | ctx.invoke(cmd, name=name, full=full, skip_import=skip_import) 127 | 128 | if viewsets or api or full: 129 | from .viewsets import viewset as cmd 130 | 131 | ctx.invoke(cmd, name=name, skip_import=skip_import) 132 | 133 | generate_related_resources() 134 | 135 | 136 | @scoped(to=Scope.APP) 137 | @click.command() 138 | @click.argument("name", required=True, callback=sanitized_string_callback) 139 | @click.argument("fields", nargs=-1, required=False, callback=fields_callback) 140 | @click.option("--api", is_flag=True, help="Destroy only related api resources") 141 | @click.pass_context 142 | def scaffold(ctx, name, fields, api): 143 | """ 144 | Generate all resources for a given model. 145 | """ 146 | 147 | ctx.invoke(model, name=name, fields=fields, full=not api, api=api) 148 | -------------------------------------------------------------------------------- /src/django_clite/commands/generate/serializers.py: -------------------------------------------------------------------------------- 1 | import click 2 | import pathlib 3 | import inflection 4 | 5 | from geny.core.filesystem.files import File 6 | from geny.core.filesystem.transformations import AddLineToFile, TouchFile 7 | from django_clite.decorators.scope import scoped, Scope 8 | from django_clite.commands import command_defaults 9 | from django_clite.commands.callbacks import sanitized_string_callback 10 | 11 | 12 | @scoped(to=Scope.APP) 13 | @click.command() 14 | @click.argument("name", required=True, callback=sanitized_string_callback) 15 | @click.option( 16 | "--skip-import", 17 | is_flag=True, 18 | default=False, 19 | help="Do not import in __init__ module", 20 | ) 21 | @click.pass_context 22 | def serializer(ctx, name, skip_import): 23 | """ 24 | Generate a serializer for a given model. 25 | 26 | Checks for the existence of the specified model in models.py 27 | before attempting to create a serializer for it. Aborts if model is not found. 28 | """ 29 | 30 | file = File( 31 | name=f"serializers/{name}.py", 32 | template="serializer.tpl", 33 | context={ 34 | "name": name, 35 | "classname": inflection.camelize(name), 36 | }, 37 | ) 38 | 39 | after_hooks = [TouchFile("serializers/__init__.py")] 40 | 41 | if not skip_import: 42 | after_hooks.append( 43 | AddLineToFile( 44 | pathlib.Path("serializers/__init__.py"), 45 | command_defaults.serializer(name), 46 | prevent_duplicates=True, 47 | ) 48 | ) 49 | 50 | file.create(after_hooks=after_hooks, **ctx.obj) 51 | -------------------------------------------------------------------------------- /src/django_clite/commands/generate/signals.py: -------------------------------------------------------------------------------- 1 | import click 2 | import pathlib 3 | 4 | from geny.core.filesystem.files import File 5 | from geny.core.filesystem.transformations import AddLineToFile, TouchFile 6 | from django_clite.decorators.scope import scoped, Scope 7 | from django_clite.commands import command_defaults 8 | from django_clite.commands.callbacks import sanitized_string_callback 9 | 10 | 11 | @scoped(to=Scope.APP) 12 | @click.command() 13 | @click.argument("name", required=True, callback=sanitized_string_callback) 14 | @click.option( 15 | "--skip-import", 16 | is_flag=True, 17 | default=False, 18 | help="Do not import in __init__ module", 19 | ) 20 | @click.pass_context 21 | def signal(ctx, name, skip_import): 22 | """ 23 | Generate a signal. 24 | """ 25 | 26 | file = File( 27 | name=f"models/signals/{name}.py", 28 | template="models/signal.tpl", 29 | context={ 30 | "name": name, 31 | }, 32 | ) 33 | 34 | after_hooks = [TouchFile("models/signals/__init__.py")] 35 | 36 | if not skip_import: 37 | after_hooks.append( 38 | AddLineToFile( 39 | pathlib.Path("models/signals/__init__.py"), 40 | command_defaults.signal(name), 41 | prevent_duplicates=True, 42 | ) 43 | ) 44 | 45 | file.create(after_hooks=after_hooks, **ctx.obj) 46 | -------------------------------------------------------------------------------- /src/django_clite/commands/generate/tags.py: -------------------------------------------------------------------------------- 1 | import click 2 | import pathlib 3 | 4 | from geny.core.filesystem.files import File 5 | from geny.core.filesystem.transformations import AddLineToFile, TouchFile 6 | from django_clite.decorators.scope import scoped, Scope 7 | from django_clite.commands import command_defaults 8 | from django_clite.commands.callbacks import sanitized_string_callback 9 | 10 | 11 | @scoped(to=Scope.APP) 12 | @click.command() 13 | @click.argument("name", required=True, callback=sanitized_string_callback) 14 | @click.option( 15 | "--skip-import", 16 | is_flag=True, 17 | default=False, 18 | help="Do not import in __init__ module", 19 | ) 20 | @click.pass_context 21 | def tag(ctx, name, skip_import): 22 | """ 23 | Generate a template tag. 24 | """ 25 | 26 | file = File( 27 | name=f"templatetags/{name}.py", 28 | template="templatetags/tag.tpl", 29 | context={ 30 | "name": name, 31 | }, 32 | ) 33 | after_hooks = [TouchFile("templatetags/__init__.py")] 34 | 35 | if not skip_import: 36 | after_hooks.append( 37 | AddLineToFile( 38 | pathlib.Path("templatetags/__init__.py"), 39 | command_defaults.tag(name), 40 | prevent_duplicates=True, 41 | ) 42 | ) 43 | 44 | file.create(after_hooks=after_hooks, **ctx.obj) 45 | -------------------------------------------------------------------------------- /src/django_clite/commands/generate/template.py: -------------------------------------------------------------------------------- 1 | import click 2 | import inflection 3 | 4 | from geny.core.filesystem.files import File 5 | from django_clite.decorators.scope import scoped, Scope 6 | from django_clite.commands.callbacks import sanitized_string_callback 7 | 8 | 9 | SUPPORTED_CLASSES = [ 10 | "create", 11 | "detail", 12 | "list", 13 | "update", 14 | ] 15 | 16 | 17 | @scoped(to=Scope.APP) 18 | @click.command(context_settings=dict(ignore_unknown_options=True)) 19 | @click.argument("name", required=True, callback=sanitized_string_callback) 20 | @click.option("--class_", type=click.Choice(SUPPORTED_CLASSES)) 21 | @click.option("--full", is_flag=True, help="Create templates for all CRUD operations") 22 | @click.pass_context 23 | def template(ctx, name, class_, full): 24 | """ 25 | Generate an html template. 26 | """ 27 | 28 | classes = SUPPORTED_CLASSES if full else [class_] 29 | 30 | for k in classes: 31 | file = File( 32 | name=f"templates/{name}{'_' + k if k else ''}.html", 33 | template=f"templates/{k if k else 'template'}.tpl", 34 | context={ 35 | "classname": inflection.camelize(name), 36 | }, 37 | ) 38 | 39 | file.create(**ctx.obj) 40 | -------------------------------------------------------------------------------- /src/django_clite/commands/generate/tests.py: -------------------------------------------------------------------------------- 1 | import click 2 | import pathlib 3 | import inflection 4 | 5 | from geny.core.filesystem.files import File 6 | from geny.core.filesystem.transformations import AddLineToFile, TouchFile 7 | from django_clite.core.logger import logger 8 | from django_clite.decorators.scope import scoped, Scope 9 | from django_clite.commands import command_defaults 10 | from django_clite.commands.callbacks import sanitized_string, sanitized_string_callback 11 | 12 | 13 | SUPPORTED_SCOPES = [ 14 | "model", 15 | "viewset", 16 | ] 17 | 18 | 19 | @scoped(to=Scope.APP) 20 | @click.command() 21 | @click.argument("name", required=True, callback=sanitized_string_callback) 22 | @click.option("--scope", required=True, type=click.Choice(SUPPORTED_SCOPES)) 23 | @click.option("--full", is_flag=True, help=f"Create tests for {SUPPORTED_SCOPES}") 24 | @click.option( 25 | "--skip-import", 26 | is_flag=True, 27 | default=False, 28 | help="Do not import in __init__ module", 29 | ) 30 | @click.pass_context 31 | def test(ctx, name, scope, full, skip_import): 32 | """ 33 | Generate TestCases. 34 | """ 35 | 36 | if scope and full: 37 | logger.error("Flags --scope and --full cannot be used simultaneously.") 38 | raise click.Abort() 39 | 40 | scopes = SUPPORTED_SCOPES if full else [scope] 41 | 42 | for s in scopes: 43 | filename = f"tests/{inflection.pluralize(s)}/{sanitized_string(name)}_test.py" 44 | 45 | file = File( 46 | name=filename, 47 | template=f"{inflection.pluralize(s)}/test.tpl", 48 | context={ 49 | "name": name, 50 | "module": name, 51 | "classname": inflection.camelize(name), 52 | "namespace": inflection.pluralize(name), 53 | "scope": "" if scope is None else inflection.pluralize(scope), 54 | }, 55 | ) 56 | 57 | after_hooks = [TouchFile(f"tests/{inflection.pluralize(s)}/__init__.py")] 58 | 59 | if not skip_import: 60 | after_hooks.append( 61 | AddLineToFile( 62 | pathlib.Path(f"tests/{inflection.pluralize(s)}/__init__.py"), 63 | command_defaults.test(name), 64 | prevent_duplicates=True, 65 | ) 66 | ) 67 | 68 | file.create(after_hooks=after_hooks, **ctx.obj) 69 | -------------------------------------------------------------------------------- /src/django_clite/commands/generate/validators.py: -------------------------------------------------------------------------------- 1 | import click 2 | import pathlib 3 | 4 | from geny.core.filesystem.files import File 5 | from geny.core.filesystem.transformations import AddLineToFile, TouchFile 6 | from django_clite.decorators.scope import scoped, Scope 7 | from django_clite.commands import command_defaults 8 | from django_clite.commands.callbacks import sanitized_string_callback 9 | 10 | 11 | @scoped(to=Scope.APP) 12 | @click.command() 13 | @click.argument("name", required=True, callback=sanitized_string_callback) 14 | @click.option( 15 | "--skip-import", 16 | is_flag=True, 17 | default=False, 18 | help="Do not import in __init__ module", 19 | ) 20 | @click.pass_context 21 | def validator(ctx, name, skip_import): 22 | """ 23 | Generate a validator. 24 | """ 25 | 26 | file = File( 27 | name=f"models/validators/{name}.py", 28 | template="models/validator.tpl", 29 | context={ 30 | "name": name, 31 | }, 32 | ) 33 | 34 | after_hooks = [TouchFile("models/validators/__init__.py")] 35 | 36 | if not skip_import: 37 | after_hooks.append( 38 | AddLineToFile( 39 | pathlib.Path("models/validators/__init__.py"), 40 | command_defaults.validator(name), 41 | prevent_duplicates=True, 42 | ) 43 | ) 44 | 45 | file.create(after_hooks=after_hooks, **ctx.obj) 46 | -------------------------------------------------------------------------------- /src/django_clite/commands/generate/views.py: -------------------------------------------------------------------------------- 1 | import click 2 | import pathlib 3 | import inflection 4 | 5 | from geny.core.filesystem.files import File 6 | from geny.core.filesystem.transformations import AddLineToFile, TouchFile 7 | from django_clite.decorators.scope import scoped, Scope 8 | from .. import command_defaults 9 | from django_clite.commands.callbacks import sanitized_string_callback 10 | 11 | from .template import template 12 | 13 | 14 | SUPPORTED_CLASSES = [ 15 | "create", 16 | "detail", 17 | "list", 18 | "update", 19 | ] 20 | 21 | 22 | @scoped(to=Scope.APP) 23 | @click.command() 24 | @click.argument("name", required=True, callback=sanitized_string_callback) 25 | @click.option("--class_", type=click.Choice(SUPPORTED_CLASSES)) 26 | @click.option("--full", is_flag=True, help="Create all CRUD views") 27 | @click.option( 28 | "--skip-templates", 29 | is_flag=True, 30 | default=False, 31 | help="Skip generation of related templates.", 32 | ) 33 | @click.option( 34 | "--skip-import", 35 | is_flag=True, 36 | default=False, 37 | help="Do not import in __init__ module", 38 | ) 39 | @click.pass_context 40 | def view(ctx, name, class_, full, skip_templates, skip_import): 41 | """ 42 | Generate a view function or class. 43 | """ 44 | 45 | classes = SUPPORTED_CLASSES if full else [class_] 46 | 47 | for k in classes: 48 | file = File( 49 | name=f"views/{name}{'_' + k if k else ''}.py", 50 | template=f"views/{k if k else 'view'}.tpl", 51 | context={ 52 | "name": name, 53 | "classname": inflection.camelize(name), 54 | "namespace": inflection.pluralize(name), 55 | "template_name": f"{name}{'_' + k if k else ''}.hml", 56 | }, 57 | ) 58 | 59 | after_hooks = [TouchFile("views/__init__.py")] 60 | 61 | if not skip_import: 62 | after_hooks.append( 63 | AddLineToFile( 64 | pathlib.Path("views/__init__.py"), 65 | command_defaults.view(name, klass=k), 66 | prevent_duplicates=True, 67 | ) 68 | ) 69 | 70 | file.create(after_hooks=after_hooks, **ctx.obj) 71 | 72 | if skip_templates: 73 | return 74 | 75 | for class_ in classes: 76 | ctx.invoke(template, name=name, class_=class_) 77 | -------------------------------------------------------------------------------- /src/django_clite/commands/generate/viewsets.py: -------------------------------------------------------------------------------- 1 | import click 2 | import pathlib 3 | import inflection 4 | 5 | from geny.core.filesystem.files import File 6 | from geny.core.filesystem.transformations import AddLineToFile, TouchFile 7 | from django_clite.decorators.scope import scoped, Scope 8 | from django_clite.commands import command_defaults 9 | from django_clite.commands.callbacks import sanitized_string_callback 10 | 11 | 12 | @scoped(to=Scope.APP) 13 | @click.command() 14 | @click.argument("name", required=True, callback=sanitized_string_callback) 15 | @click.option("--read-only", is_flag=True, help="Create a read-only viewset.") 16 | @click.option("--full", is_flag=True, help="Create related files (i.e. TestCases)") 17 | @click.option( 18 | "--skip-import", 19 | is_flag=True, 20 | default=False, 21 | help="Do not import in __init__ module", 22 | ) 23 | @click.pass_context 24 | def viewset(ctx, name, read_only, full, skip_import): 25 | """ 26 | Generate a viewset for a serializable model. 27 | """ 28 | 29 | file = File( 30 | name=f"viewsets/{name}.py", 31 | template="viewsets/viewset.tpl", 32 | context={ 33 | "name": name, 34 | "module": name, 35 | "read_only": read_only, 36 | "namespace": inflection.pluralize(name), 37 | "classname": inflection.camelize(name), 38 | }, 39 | ) 40 | 41 | # TODO: Create router file 42 | # router = Directory( 43 | # "router", 44 | # children=[ 45 | # File(name="__init__.py", template="app/router_init.tpl"), 46 | # File(name="api.py", template="app/router_api.tpl"), 47 | # File(name="router.py", template="app/router.tpl"), 48 | # ], 49 | # ) 50 | # 51 | # router.create(**ctx.obj) 52 | 53 | after_hooks = [TouchFile("viewsets/__init__.py")] 54 | 55 | if not skip_import: 56 | after_hooks.append( 57 | AddLineToFile( 58 | pathlib.Path("viewsets/__init__.py"), 59 | command_defaults.viewset(name), 60 | prevent_duplicates=True, 61 | ) 62 | ) 63 | 64 | file.create(after_hooks=after_hooks, **ctx.obj) 65 | 66 | if full: 67 | from .tests import test as cmd 68 | 69 | ctx.invoke(cmd, name=name, scope="viewset", skip_import=skip_import) 70 | -------------------------------------------------------------------------------- /src/django_clite/commands/new/__init__.py: -------------------------------------------------------------------------------- 1 | # django_clite:commands:new 2 | -------------------------------------------------------------------------------- /src/django_clite/commands/new/app.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from django_clite.constants import ENABLE_DRY_RUN_KEY 4 | from .defaults.app import application_callback 5 | 6 | 7 | @click.command() 8 | @click.argument("names", nargs=-1, callback=application_callback) 9 | @click.option("--is-package", is_flag=True, help="Treat as a standalone Python package") 10 | @click.pass_context 11 | def apps(ctx, names, is_package): 12 | """ 13 | Creates new django apps. 14 | 15 | This is similar to using `django-admin startapp app_name` 16 | but adds more structure to your app by creating packages for forms, models, 17 | serializers, tests, templates, views, and viewsets. 18 | 19 | The command can accept multiple apps as arguments. Like so: 20 | 21 | django-clite new apps shop blog forum 22 | 23 | The command above will create all 4 apps two levels within your project's 24 | directory, i.e. myproject/myproject. The CLI tries to identify where the management module for your 25 | project is, so it can place your app files in the correct location. This helps with consistency 26 | as the CLI can infer module/package scopes when performing other automatic configurations. 27 | 28 | As part of the CLI convention, each app is assigned its own `urls.py` file, which can be used to route urls on a 29 | per-app basis. 30 | Another convention the CLI adopts is to add a viewsets package to the app's directory by default (for use with DRF). 31 | Within the viewsets directory, a DRF router is instantiated in `router.py` and its urls added to each app's 32 | urlpatterns by default. 33 | """ 34 | 35 | if ctx.obj.get(ENABLE_DRY_RUN_KEY): 36 | return 37 | 38 | for application in names: 39 | context = dict(ctx.obj) 40 | context.update({"app": application.name}) 41 | application.create(**context) 42 | -------------------------------------------------------------------------------- /src/django_clite/commands/new/defaults/__init__.py: -------------------------------------------------------------------------------- 1 | # django_clite:commands:new:defaults 2 | -------------------------------------------------------------------------------- /src/django_clite/commands/new/defaults/app.py: -------------------------------------------------------------------------------- 1 | # # django_clite:commands:new:defaults:app 2 | from geny.core.filesystem.files import File 3 | from geny.core.filesystem.directories import Directory 4 | 5 | 6 | def generic_package(n: str) -> Directory: 7 | return Directory(n, children=[File(name="__init__.py", template="shared/init.tpl")]) 8 | 9 | 10 | def new_app(name: str) -> Directory: 11 | admin = Directory( 12 | name="admin", 13 | children=[ 14 | File(name="__init__.py", template="shared/init.tpl"), 15 | generic_package("actions"), 16 | generic_package("inlines"), 17 | generic_package("permissions"), 18 | ], 19 | ) 20 | 21 | models = Directory( 22 | "models", 23 | children=[ 24 | File(name="__init__.py", template="shared/init.tpl"), 25 | generic_package("managers"), 26 | generic_package("signals"), 27 | generic_package("validators"), 28 | ], 29 | ) 30 | 31 | router = Directory( 32 | "router", 33 | children=[ 34 | File(name="__init__.py", template="app/router_init.tpl"), 35 | File(name="urls.py", template="app/router_urls.tpl"), 36 | ], 37 | ) 38 | 39 | tests = Directory( 40 | "tests", 41 | children=[ 42 | File( 43 | name="__init__.py", 44 | content="from .models import *\nfrom .viewsets import *", 45 | ), 46 | generic_package("models"), 47 | generic_package("viewsets"), 48 | ], 49 | ) 50 | 51 | viewsets = Directory( 52 | "viewsets", 53 | children=[ 54 | Directory(name="permissions"), 55 | Directory(name="mixins"), 56 | File(name="__init__.py", template="shared/init.tpl"), 57 | ], 58 | ) 59 | 60 | app = Directory( 61 | name=name, 62 | children=[ 63 | admin, 64 | models, 65 | router, 66 | tests, 67 | viewsets, 68 | generic_package("fixtures"), 69 | generic_package("forms"), 70 | generic_package("middleware"), 71 | generic_package("migrations"), 72 | generic_package("serializers"), 73 | generic_package("tasks"), 74 | generic_package("templates"), 75 | generic_package("templatetags"), 76 | generic_package("views"), 77 | File(name="__init__.py", template="shared/init.tpl"), 78 | File(name="apps.py", template="app/apps.tpl"), 79 | File(name="urls.py", template="app/urls.tpl"), 80 | File(name="constants.py", content=""), 81 | ], 82 | ) 83 | 84 | return app 85 | 86 | 87 | def application_callback(ctx, param, value) -> list[Directory]: 88 | apps = [] 89 | 90 | for name in value: 91 | app = new_app(name) 92 | 93 | if ctx.params.get("is_package", False): 94 | app = Directory( 95 | name=name, 96 | children=[ 97 | app, 98 | File(name="LICENSE", template="shared/LICENSE.tpl"), 99 | File(name="MANIFEST", template="shared/MANIFEST.tpl"), 100 | File(name="README.md", template="shared/README.tpl"), 101 | File(name="pyproject.toml", template="shared/pyproject.tpl"), 102 | ], 103 | ) 104 | 105 | apps.append(app) 106 | 107 | return apps 108 | -------------------------------------------------------------------------------- /src/django_clite/commands/new/defaults/project.py: -------------------------------------------------------------------------------- 1 | # django_clite:commands:new:defaults:project 2 | from geny.core.filesystem.files import File 3 | from geny.core.filesystem.directories import Directory 4 | from geny.core.filesystem.transformations import MoveFile 5 | 6 | project_transformations = [] 7 | 8 | 9 | def new_project(name: str, **context) -> Directory: 10 | project_transformations.append( 11 | MoveFile("settings.py", "settings/__init__.py"), 12 | ) 13 | 14 | proj = Directory( 15 | name=name, 16 | children=[ 17 | Directory( 18 | name="settings", 19 | children=[ 20 | File(name="api.py", template="project/api.tpl"), 21 | ], 22 | ), 23 | File(name="constants.py", content="# your constants go here"), 24 | ], 25 | ) 26 | 27 | root = Directory( 28 | name=name, 29 | children=[ 30 | proj, 31 | Directory(name="staticfiles"), 32 | Directory( 33 | name="templates", 34 | children=[ 35 | File(name="404.html", template="app/404.tpl"), 36 | File(name="500.html", template="app/500.tpl"), 37 | ], 38 | ), 39 | File(name=".env", template="project/env.tpl"), 40 | File(name=".gitignore", template="project/gitignore.tpl"), 41 | File(name="README.md", template="project/README.tpl"), 42 | File(name="requirements.txt", template="project/requirements.tpl"), 43 | ], 44 | ) 45 | 46 | # Handle user options 47 | 48 | if context.get("github", False): 49 | github = Directory( 50 | ".github", 51 | children=[ 52 | Directory( 53 | ".github", 54 | children=[ 55 | File(name="ci.yml", template="github/ci.tpl"), 56 | File(name="pull_request_template.md", template="github/pull_request_template.tpl"), 57 | ], 58 | ) 59 | ], 60 | ) 61 | 62 | root.add_children(github) # noqa 63 | 64 | if context.get("docker", False): 65 | docker = [ 66 | File(name=".dockerignore", template="docker/dockerignore.tpl"), 67 | File(name="Dockerfile", template="docker/dockerfile.tpl"), 68 | File(name="docker-compose.yml", template="docker/docker-compose.tpl"), 69 | File(name="docker-entrypoint.sh", template="docker/docker-entrypoint.tpl"), 70 | ] 71 | 72 | root.add_children(docker) # noqa 73 | 74 | if context.get("kubernetes", False): 75 | kubernetes = [ 76 | Directory( 77 | ".kubernetes", 78 | children=[ 79 | File(name="deployment.yaml", template="kubernetes/deployment.tpl"), 80 | File(name="service.yaml", template="kubernetes/service.tpl"), 81 | File(name="ingress.yaml", template="kubernetes/ingress.tpl"), 82 | File(name="configmap.yaml", template="kubernetes/configmap.tpl"), 83 | ], 84 | ), 85 | ] 86 | 87 | root.add_children(kubernetes) # noqa 88 | 89 | # TODO: implement celery option 90 | # if options.get("celery", False): 91 | 92 | # TODO: implement drf option 93 | # if options.get("drf", False): 94 | 95 | # TODO: implement dokku option 96 | # if options.get("dokku", False): 97 | 98 | # TODO: implement heroku option 99 | # if options.get("heroku", False): 100 | 101 | return root 102 | -------------------------------------------------------------------------------- /src/django_clite/commands/new/main.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from django_clite.commands.new.app import apps 4 | from django_clite.commands.new.project import project 5 | 6 | 7 | @click.group() 8 | @click.pass_context 9 | def new(ctx): 10 | """ 11 | Create projects and apps. 12 | """ 13 | 14 | ctx.ensure_object(dict) 15 | 16 | 17 | [new.add_command(cmd) for cmd in [apps, project]] 18 | -------------------------------------------------------------------------------- /src/django_clite/commands/new/project.py: -------------------------------------------------------------------------------- 1 | import click 2 | from pathlib import Path 3 | from django.core.management import call_command, CommandError 4 | from django.core.management.commands import startproject 5 | 6 | from geny.core.filesystem.filesystem import working_directory 7 | from django_clite.commands.callbacks import sanitized_string_callback 8 | from django_clite.constants import ENABLE_DRY_RUN_KEY 9 | 10 | from .defaults.project import new_project, project_transformations 11 | from .defaults.app import application_callback 12 | from .app import apps as apps_cmd 13 | 14 | 15 | @click.command() 16 | @click.argument("name", callback=sanitized_string_callback) 17 | @click.argument("apps", nargs=-1, callback=application_callback) 18 | @click.option( 19 | "--docker", is_flag=True, help="Render Dockerfile and other docker-related files." 20 | ) 21 | @click.option("--github", is_flag=True, help="Render GitHub CI template.") 22 | @click.option("--kubernetes", is_flag=True, help="Render Kubernetes deployment files.") 23 | # @click.option("--dokku", is_flag=True, help="Render Dokku deployment files.") 24 | # @click.option("--heroku", is_flag=True, help="Render Heroku files.") 25 | @click.pass_context 26 | def project(ctx, name, apps, docker, github, kubernetes): # TODO: dokku, heroku 27 | """ 28 | Creates a new django project. 29 | 30 | This is similar to using `django-admin startproject project_name` but with some added functionality. 31 | The command can handle the creation of apps upon creation of any project, you can simply specify the name of 32 | the apps after the project name: 33 | 34 | django-clite new project myproject app1 app2 app3 ... 35 | """ 36 | 37 | if ctx.obj.get(ENABLE_DRY_RUN_KEY): 38 | return 39 | 40 | cmd = startproject.Command() 41 | 42 | try: 43 | call_command(cmd, name, verbosity=0) 44 | 45 | # Generate project files as per CLI conventions 46 | params = dict(ctx.params) 47 | params.pop("name") 48 | params.pop("apps") 49 | 50 | click_ctx = dict(ctx.obj) 51 | 52 | context = {"port": "8080", "workers": 1, "project": name} 53 | context.update(**click_ctx) 54 | context.update(**params) 55 | 56 | proj = new_project(name, **context) 57 | proj.create(**context) 58 | 59 | dir_ = Path(name) / name # i.e. myproject/myproject 60 | with working_directory(dir_): 61 | for t in project_transformations: 62 | # Customize django-generated files 63 | t.run() 64 | 65 | # Create nested apps 66 | with working_directory(name): 67 | ctx.invoke(apps_cmd, names=apps) 68 | 69 | # TODO: Initialize git repository? 70 | 71 | except CommandError as err: 72 | click.echo(f"Command returned an error {repr(err)}") 73 | raise click.Abort() 74 | -------------------------------------------------------------------------------- /src/django_clite/constants.py: -------------------------------------------------------------------------------- 1 | # django_clite 2 | 3 | # CLI configuration 4 | 5 | CLI_NAME_KEY = "django-clite" 6 | DJANGO_FILES_KEY = "django_files" 7 | ENABLE_DEBUG_KEY = "enable_debug" 8 | ENABLE_DRY_RUN_KEY = "enable_dry" 9 | ENABLE_FORCE_KEY = "GENY_ENABLE_FORCE" 10 | ENABLE_VERBOSITY_KEY = "enable_verbosity" 11 | 12 | TEMPLATES_KEY = "templates" 13 | TEMPLATES_DIRECTORY_ENV_VAR = "DJANGO_CLITE_TEMPLATES_DIR" 14 | PLUGINS_ENV_VAR = "DJANGO_CLITE_PLUGINS" 15 | 16 | DEV_MODE_ENV_VAR = "DJANGO_CLITE_DEV_MODE" 17 | 18 | # Project information 19 | 20 | APPLICATION_NAME_KEY = "app" 21 | PROJECT_NAME_KEY = "project" 22 | -------------------------------------------------------------------------------- /src/django_clite/core/__init__.py: -------------------------------------------------------------------------------- 1 | # django_clite:core 2 | from .field_parser.factory import AttributeFactory, FieldOptions # noqa 3 | from .git.git import Git, GitHandler # noqa 4 | from .logger import logger # noqa 5 | -------------------------------------------------------------------------------- /src/django_clite/core/field_parser/__init__.py: -------------------------------------------------------------------------------- 1 | # django_clite:core:field_parser 2 | from .factory import AttributeFactory, FieldOptions # noqa: F401 3 | -------------------------------------------------------------------------------- /src/django_clite/core/field_parser/factory.py: -------------------------------------------------------------------------------- 1 | # django_clite:core:field_parser 2 | import inflection 3 | 4 | from dataclasses import dataclass 5 | from geny.core.decorators.singleton import singleton 6 | 7 | 8 | json_compatible_fields = { 9 | "BigIntegerField": "pyint", 10 | "BooleanField": "pybool", 11 | "CharField": "text", 12 | "DateField": "future_date", 13 | "DateTimeField": "iso8601", 14 | "DecimalField": "pydecimal", 15 | "EmailField": "safe_email", 16 | "GenericIPAddressField": "ipv4", 17 | "FileField": "file_path", 18 | "FilePathField": "file_path", 19 | "FloatField": "pyfloat", 20 | "ImageField": "image_url", 21 | "IntegerField": "pyint", 22 | "SlugField": "slug", 23 | "TextField": "text", 24 | "TimeField": "time", 25 | "URLField": "url", 26 | "UUIDField": "uuid4", 27 | } 28 | 29 | 30 | @dataclass 31 | class FieldOptions: 32 | kind: str 33 | options: dict = None 34 | supports_admin: bool = True 35 | is_fk_relationship: bool = False 36 | is_many_relationship: bool = False 37 | 38 | @property 39 | def is_relationship(self) -> bool: 40 | return self.is_fk_relationship or self.is_many_relationship 41 | 42 | @property 43 | def is_media_field(self) -> bool: 44 | return self.kind in ["FileField", "FilePathField", "ImageField"] 45 | 46 | @classmethod 47 | def klass_name(cls, attr_name): 48 | return inflection.camelize(inflection.singularize(attr_name)).strip() 49 | 50 | @classmethod 51 | def module_name(cls, attr_name): 52 | return inflection.singularize(attr_name).strip() 53 | 54 | @classmethod 55 | def upload_path(cls, attr_name, model_name) -> str: 56 | if not cls.is_media_field: 57 | return "" 58 | 59 | return f"uploads/{inflection.pluralize(model_name.lower())}/{inflection.pluralize(attr_name)}/" 60 | 61 | @property 62 | def example_value(self): 63 | x = json_compatible_fields.get(self.kind, "") 64 | return x 65 | 66 | def field_options(self, attr_name: str, model_name: str): 67 | if self.options is None: 68 | self.options = {} 69 | 70 | if self.is_relationship: 71 | self.options.update( 72 | { 73 | "related_name": f"'{inflection.pluralize(inflection.dasherize(model_name.lower()))}'" 74 | } 75 | ) 76 | 77 | options = [self.klass_name(attr_name)] if self.is_relationship else [] 78 | options.append(f"_('{attr_name}')") 79 | 80 | if self.options is None: 81 | return "" 82 | 83 | for k, v in self.options.items(): 84 | options.append(f"{k}={v}") 85 | 86 | if self.is_media_field: 87 | options.append(f"upload_to='{self.upload_path(attr_name, model_name)}'") 88 | 89 | return ", ".join(options) 90 | 91 | 92 | @singleton 93 | class AttributeFactory: 94 | def __init__(self, associations: dict[str, FieldOptions], aliases: dict[str, str]): 95 | self.registry = associations 96 | self.aliases = aliases 97 | 98 | def field_options(self, kind: str, name: str) -> FieldOptions: 99 | if kind == "": 100 | return None 101 | 102 | options = self.registry.get(kind, None) 103 | 104 | if options is None: 105 | alias = self.aliases.get(kind, None) 106 | options = self.registry.get(alias, None) 107 | 108 | if options is None: 109 | return None 110 | 111 | return options 112 | 113 | def parsed_fields(self, values: list[str]) -> dict[str, FieldOptions]: 114 | pairs = dict(arg.split(":") for arg in values) 115 | 116 | fields = {} 117 | for n, k in pairs.items(): 118 | f = self.field_options(k, n) 119 | fields[n] = f 120 | 121 | return fields 122 | 123 | 124 | attribute_aliases = { 125 | "bigint": "big", 126 | "big-int": "big", 127 | "integer": "int", 128 | "boolean": "bool", 129 | "string": "text", 130 | "file-path": "filepath", 131 | "photo": "image", 132 | "ipaddress": "ip", 133 | "ip-address": "ip", 134 | "belongsto": "fk", 135 | "belongs-to": "fk", 136 | "foreignkey": "fk", 137 | "foreign-key": "fk", 138 | "one-to-one": "one", 139 | "hasone": "one", 140 | "has-one": "one", 141 | "many": "hasmany", 142 | "has-many": "hasmany", 143 | "manytomany": "hasmany", 144 | "many-to-many": "hasmany", 145 | } 146 | 147 | field_registry = { 148 | # Relationships 149 | "fk": FieldOptions( 150 | kind="ForeignKey", 151 | options={"blank": False, "on_delete": "models.DO_NOTHING"}, 152 | is_fk_relationship=True, 153 | ), 154 | "one": FieldOptions( 155 | kind="OneToOneField", 156 | options={"blank": False, "on_delete": "models.CASCADE", "primary_key": True}, 157 | is_fk_relationship=True, 158 | ), 159 | "hasmany": FieldOptions( 160 | kind="ManyToManyField", 161 | options={"blank": True, "on_delete": "models.DO_NOTHING"}, 162 | supports_admin=False, 163 | is_many_relationship=True, 164 | ), 165 | # Boolean 166 | "bool": FieldOptions(kind="BooleanField", options={"default": False}), 167 | # Numeric 168 | "big": FieldOptions(kind="BigIntegerField"), 169 | "decimal": FieldOptions( 170 | kind="DecimalField", options={"decimal_places": 2, "max_digits": 8} 171 | ), 172 | "float": FieldOptions(kind="FloatField"), 173 | "int": FieldOptions(kind="IntegerField"), 174 | # Text 175 | "char": FieldOptions(kind="CharField", options={"max_length": 100, "blank": False}), 176 | "email": FieldOptions(kind="EmailField"), 177 | "slug": FieldOptions(kind="SlugField", options={"unique": True}), 178 | "text": FieldOptions( 179 | kind="TextField", options={"blank": False}, supports_admin=False 180 | ), 181 | "url": FieldOptions(kind="URLField"), 182 | "uuid": FieldOptions( 183 | kind="UUIDField", options={"default": "uuid.uuid4", "editable": False} 184 | ), 185 | # Files 186 | "file": FieldOptions( 187 | kind="FileField", options={"blank": False}, supports_admin=False 188 | ), 189 | "filepath": FieldOptions( 190 | kind="FilePathField", options={"blank": True}, supports_admin=False 191 | ), 192 | "image": FieldOptions( 193 | kind="ImageField", options={"blank": False}, supports_admin=False 194 | ), 195 | # Date 196 | "date": FieldOptions(kind="DateField", options={"auto_now": True}), 197 | "datetime": FieldOptions(kind="DateTimeField", options={"auto_now": True}), 198 | "duration": FieldOptions(kind="DurationField"), 199 | "time": FieldOptions(kind="TimeField", options={"auto_now": True}), 200 | # Ip 201 | "ip": FieldOptions(kind="GenericIPAddressField"), 202 | } 203 | 204 | # Singleton 205 | AttributeFactory(field_registry, attribute_aliases) 206 | -------------------------------------------------------------------------------- /src/django_clite/core/git/__init__.py: -------------------------------------------------------------------------------- 1 | # django_clite:core:git 2 | -------------------------------------------------------------------------------- /src/django_clite/core/git/git.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | from .protocols import Git 4 | from geny.core.decorators.singleton import singleton 5 | 6 | 7 | @singleton 8 | class GitHandler(Git): 9 | def initialize(self) -> bool: 10 | try: 11 | subprocess.check_output(["git", "init"]) 12 | subprocess.check_output(["git", "add", "--all"]) 13 | subprocess.check_output(["git", "commit", "-m", "feat: initial commit"]) 14 | return True 15 | except subprocess.CalledProcessError as _: 16 | pass 17 | 18 | return False 19 | 20 | def add_origin(self, origin: str) -> bool: 21 | try: 22 | subprocess.check_output(["git", "remote", "add", "origin", origin]) 23 | return True 24 | except subprocess.CalledProcessError: 25 | pass 26 | 27 | return False 28 | -------------------------------------------------------------------------------- /src/django_clite/core/git/protocols.py: -------------------------------------------------------------------------------- 1 | # django_clite:core 2 | from typing import Protocol 3 | 4 | 5 | class Git(Protocol): 6 | def initialize(self) -> bool: 7 | ... 8 | 9 | def add_origin(self, origin: str) -> bool: 10 | ... 11 | -------------------------------------------------------------------------------- /src/django_clite/core/logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from django_clite.constants import CLI_NAME_KEY 3 | 4 | 5 | logger = logging.getLogger(CLI_NAME_KEY) 6 | -------------------------------------------------------------------------------- /src/django_clite/decorators/__init__.py: -------------------------------------------------------------------------------- 1 | # django_clite:decorators 2 | -------------------------------------------------------------------------------- /src/django_clite/decorators/scope.py: -------------------------------------------------------------------------------- 1 | # django_clite:decorators:scope 2 | import os 3 | 4 | import click 5 | from enum import Enum 6 | 7 | from geny.core.filesystem.finder import core_project_files 8 | from django_clite.constants import ENABLE_FORCE_KEY 9 | 10 | 11 | class Scope(Enum): 12 | APP = "app" 13 | PROJECT = "project" 14 | 15 | 16 | def scoped(to: Scope): 17 | def decorator(cmd: click.Command): 18 | def allowed_to_continue() -> bool: 19 | django_files = core_project_files(os.getcwd()) 20 | 21 | # 1. Probably not inside a django project directory 22 | if len(django_files) == 0: 23 | return False 24 | 25 | # 2. Possibly inside a django project, but no app detected 26 | if to == Scope.APP and django_files.get("apps.py", None): 27 | return True 28 | 29 | # 3. No django project detected 30 | matched_project_files = [ 31 | x 32 | for x in django_files.keys() 33 | if x in ["manage.py", "wsgi.py", "asgi.py"] 34 | ] 35 | if to == Scope.PROJECT and len(matched_project_files) > 0: 36 | return True 37 | 38 | return False 39 | 40 | class ScopedCommand(click.Command): 41 | def invoke(self, ctx): 42 | if ctx.obj.get(ENABLE_FORCE_KEY, False) or allowed_to_continue(): 43 | super().invoke(ctx) 44 | return 45 | 46 | click.echo( 47 | f"Command {cmd.name} has '{to.value}' scope but the {to.value} was not detected", 48 | err=True, 49 | ) 50 | raise click.Abort() 51 | 52 | return ScopedCommand( 53 | add_help_option=cmd.add_help_option, 54 | callback=cmd.callback, 55 | context_settings=cmd.context_settings, 56 | deprecated=cmd.deprecated, 57 | epilog=cmd.epilog, 58 | help=cmd.help, 59 | hidden=cmd.hidden, 60 | name=cmd.name, 61 | no_args_is_help=cmd.no_args_is_help, 62 | options_metavar=cmd.options_metavar, 63 | params=cmd.params, 64 | short_help=cmd.short_help, 65 | ) 66 | 67 | return decorator 68 | -------------------------------------------------------------------------------- /src/django_clite/extensions/__init__.py: -------------------------------------------------------------------------------- 1 | # django_clite:extensions 2 | from .aliased import AliasedGroup # noqa 3 | from .discoverable import DiscoverableGroup # noqa 4 | from .combined import AliasedAndDiscoverableGroup # noqa 5 | -------------------------------------------------------------------------------- /src/django_clite/extensions/aliased.py: -------------------------------------------------------------------------------- 1 | from click import Group 2 | 3 | 4 | class AliasedGroup(Group): 5 | """ 6 | Adds support for abbreviated commands 7 | """ 8 | 9 | def get_command(self, ctx, cmd_name): 10 | rv = Group.get_command(self, ctx, cmd_name) 11 | 12 | if rv is not None: 13 | return rv 14 | 15 | matches = [ 16 | match for match in self.list_commands(ctx) if match.startswith(cmd_name) 17 | ] 18 | 19 | if not matches: 20 | return None 21 | 22 | if len(matches) != 1: 23 | raise ctx.fail("Too many matches: %s" % ", ".join(sorted(matches))) 24 | 25 | return Group.get_command(self, ctx, matches[0]) 26 | -------------------------------------------------------------------------------- /src/django_clite/extensions/combined.py: -------------------------------------------------------------------------------- 1 | from django_clite.extensions.discoverable import DiscoverableGroup 2 | 3 | 4 | class AliasedAndDiscoverableGroup(DiscoverableGroup): 5 | """ 6 | Combines support for abbreviated and auto-discoverable commands/plugins 7 | """ 8 | 9 | def __get_command(self, ctx, cmd_name): 10 | return DiscoverableGroup.get_command(self, ctx, cmd_name) 11 | 12 | def get_command(self, ctx, cmd_name): 13 | matches = [x for x in self.list_commands(ctx) if x.startswith(cmd_name)] 14 | 15 | if not matches: 16 | return None 17 | 18 | if len(matches) != 1: 19 | raise ctx.fail("Too many matches: %s" % ", ".join(sorted(matches))) 20 | 21 | return self.__get_command(ctx, matches[0]) 22 | -------------------------------------------------------------------------------- /src/django_clite/extensions/discoverable.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | from click import MultiCommand 4 | from django_clite.constants import PLUGINS_ENV_VAR 5 | from django_clite.constants import DEV_MODE_ENV_VAR 6 | 7 | COMMANDS_FOLDER = Path(__file__).resolve().parent.parent.joinpath("commands") 8 | 9 | PLUGINS_FOLDER = os.environ.get(PLUGINS_ENV_VAR, None) 10 | 11 | 12 | def execute_command(command, ns): 13 | with open(command) as f: 14 | code = compile(f.read(), command, "exec") 15 | eval(code, ns, ns) 16 | 17 | 18 | class DiscoverableGroup(MultiCommand): 19 | """ 20 | Adds support for auto-discoverable commands/plugins 21 | 22 | Commands are auto-discovered if they are placed under the COMMAND_FOLDER. 23 | 24 | But please be sure to do the following for this to work: 25 | 1. Name your package and click command the same. 26 | 2. Place your command definition within your package's main.py module 27 | 3. Any sub-commands of your command should be added to the top-most command in the package's main.py module. 28 | 29 | If you would like to skip a plugin/command from being auto-discovered, 30 | simply rename the package by either prepending or appending any number of underscores (_). 31 | Any code contained within the package will be ignored. 32 | """ 33 | 34 | COMMAND_PATHS = [COMMANDS_FOLDER] 35 | 36 | def __get_commands(self): 37 | commands = {} 38 | 39 | if PLUGINS_FOLDER is not None: 40 | self.COMMAND_PATHS.append(PLUGINS_FOLDER) 41 | 42 | # Core commands + plugins 43 | for path in self.COMMAND_PATHS: 44 | c = { 45 | command_path.rsplit("/", 1)[ 46 | -1 47 | ]: command_path # command_name: command_path 48 | for command_path, _, files in os.walk(path) 49 | if "main.py" in files 50 | } 51 | 52 | commands.update(c) 53 | 54 | return commands 55 | 56 | def list_commands(self, ctx): 57 | rv = [] 58 | 59 | commands = self.__get_commands() 60 | 61 | """ 62 | Only include top-level commands. 63 | That is, if we find a directory structure like so: 64 | commands/read/main.py 65 | commands/read/file/main.py 66 | commands/read/url/main.py 67 | 68 | We will only add `read` as a command and expect `file` and `url` 69 | to have been added as sub-commands of `read` already. 70 | """ 71 | 72 | debug_mode = os.getenv(DEV_MODE_ENV_VAR, "false").lower() == "true" 73 | 74 | for func, path in commands.items(): 75 | # Skip packages beginning or ending in underscores (_) 76 | command = path.split("/")[-1] 77 | if command not in rv and not ( 78 | command.startswith("_") or command.endswith("_") 79 | ): 80 | rv.append(func) 81 | 82 | if debug_mode: 83 | print(f"{func} from {path}") 84 | 85 | rv.sort() 86 | 87 | # if debug_mode: 88 | # print(rv) 89 | 90 | return rv 91 | 92 | def get_command(self, ctx, name): 93 | ns = {} 94 | fn = os.path.join(COMMANDS_FOLDER, name, "main.py") 95 | 96 | try: 97 | execute_command(fn, ns) 98 | except FileNotFoundError: 99 | try: 100 | pfn = os.path.join(PLUGINS_FOLDER, name, "main.py") 101 | execute_command(pfn, ns) 102 | except FileNotFoundError or TypeError: 103 | pass 104 | 105 | try: 106 | return ns[name] 107 | except KeyError: 108 | # Fail gracefully if command is not found or fails to load 109 | pass 110 | -------------------------------------------------------------------------------- /src/django_clite/template_files/__init__.py: -------------------------------------------------------------------------------- 1 | # django_clite:templates 2 | -------------------------------------------------------------------------------- /src/django_clite/template_files/admin/admin.tpl: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from ..models import {{ classname }} 3 | 4 | 5 | @admin.register({{ classname }}) 6 | class {{ classname }}Admin(admin.ModelAdmin): 7 | {%- if fields %} 8 | list_display = [{% for attr_name in fields %} 9 | '{{ attr_name }}', 10 | {%- endfor %} 11 | ] 12 | {% else %} 13 | list_display = [] 14 | {% endif %} 15 | 16 | {%- if permissions %} 17 | def has_change_permission(self, request, obj=None): 18 | return request.user.is_superuser 19 | 20 | def has_delete_permission(self, request, obj=None): 21 | return request.user.is_superuser 22 | 23 | """ 24 | # For managed models, update the name of the last editor 25 | def save_model(self, request, obj, form, change): 26 | if obj.created_by_id is None: 27 | obj.created_by_id = request.user.id 28 | obj.updated_by_id = request.user 29 | super().save_model(request, obj, form, change) 30 | """ 31 | {%- endif -%} -------------------------------------------------------------------------------- /src/django_clite/template_files/admin/inline.tpl: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from ...models import {{ classname }} 3 | 4 | 5 | class {{ classname }}Inline(admin.StackedInline): 6 | model = {{ classname }} 7 | extra = 0 8 | -------------------------------------------------------------------------------- /src/django_clite/template_files/app/404.tpl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oleoneto/django-clite/c47ff018a3ff4fcc202029805fb58bc4f999fda8/src/django_clite/template_files/app/404.tpl -------------------------------------------------------------------------------- /src/django_clite/template_files/app/500.tpl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oleoneto/django-clite/c47ff018a3ff4fcc202029805fb58bc4f999fda8/src/django_clite/template_files/app/500.tpl -------------------------------------------------------------------------------- /src/django_clite/template_files/app/LICENSE.tpl: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) {{ year }}, {{ author }}. All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /src/django_clite/template_files/app/MANIFEST.tpl: -------------------------------------------------------------------------------- 1 | include LICENSE.md 2 | include README.md 3 | recursive-include {{ app }}/templates * 4 | recursive-include docs * 5 | -------------------------------------------------------------------------------- /src/django_clite/template_files/app/README.tpl: -------------------------------------------------------------------------------- 1 | # {{ app }} 2 | 3 | {{ package_description }} 4 | 5 | ![PyPI - License](https://img.shields.io/pypi/l/{{ app }}) 6 | ![PyPI - Version](https://img.shields.io/pypi/v/{{ app }}) 7 | ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/{{ app }}) 8 | ![PyPI - Downloads](https://img.shields.io/pypi/dm/{{ app }}) 9 | 10 | #### Dependencies 11 | Use of **{{ app }}** requires: 12 | - 13 | 14 | Those apps will need to be installed in the ``INSTALLED_APPS`` tuple of your django project. 15 | 16 | 17 | #### Models 18 | The app is split into the following models: 19 | - 20 | 21 | #### Installation 22 | 1. Add **{{ app }}** to your `INSTALLED_APPS` setting like this:: 23 | ```python 24 | INSTALLED_APPS = [ 25 | # other apps... 26 | '{{ app }}', 27 | ] 28 | ``` 29 | 30 | Alternatively, you can also add this app like so:: 31 | ```python 32 | INSTALLED_APPS = [ 33 | # other apps... 34 | '{{ app }}.apps.{{ app_classname }}Config', 35 | ] 36 | ``` 37 | 38 | 2. Include the polls URLconf in your project urls.py like this:: 39 | ```python 40 | path('{{ app_namespace }}/', include('{{ app }}.urls', namespace='{{ app }}')), 41 | ``` 42 | 43 | 2.1. Optionally, you can also add the api endpoints in your project urls.py like so:: 44 | ```python 45 | path('api/', include('{{ app }}.api', namespace='{{ app }}_api')), 46 | ``` 47 | 48 | 3. Run ``python manage.py migrate`` to create the app models. 49 | 50 | 4. Start the development server and visit [`http://127.0.0.1:8000/admin/`](http://127.0.0.1:8000/admin/) 51 | to start a add chat groups and messages (you'll need the Admin app enabled). 52 | 53 | 5. Visit [`http://127.0.0.1:8000/{{ app_namespace }}/`](http://127.0.0.1:8000/{{ app_namespace }}/) to use the app. 54 | You should have the following urls added to your url schemes: 55 | ``` 56 | http://127.0.0.1:8000/{{ app_namespace }}/ 57 | # list missing urls here... 58 | ``` 59 | 60 | 5.1. If you've included the api urls as well, you can visit the endpoints by visiting:: 61 | ``` 62 | http://127.0.0.1:8000/api/{{ app_namespace }} 63 | # list missing urls here... 64 | ``` 65 | 66 | ## License 67 | **{{ app }}**. [Check out the license](LICENSE). 68 | 69 | ------ 70 | 71 | Built with [django-clite](https://github.com/oleoneto/django-clite). 72 | 73 | Maintained by {{ user }} -------------------------------------------------------------------------------- /src/django_clite/template_files/app/__init__.py: -------------------------------------------------------------------------------- 1 | # django_clite:templates:app 2 | -------------------------------------------------------------------------------- /src/django_clite/template_files/app/admin-init.tpl: -------------------------------------------------------------------------------- 1 | # {{ if project }}{{ project }}:{% endif %}{{ app }}:admin 2 | from django.contrib import admin 3 | 4 | admin.site.site_header = '{{ project }}' 5 | admin.site.site_title = '{{ project }} Dashboard' 6 | admin.site.index_title = '{{ project }} Dashboard' 7 | admin.empty_value_display = '**Empty**' 8 | -------------------------------------------------------------------------------- /src/django_clite/template_files/app/apps.tpl: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class {{ app.capitalize() }}Config(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = '{% if project %}{{ project }}.{% endif %}{{ app }}' 7 | 8 | def ready(self): 9 | from .models import signals 10 | -------------------------------------------------------------------------------- /src/django_clite/template_files/app/gitignore.tpl: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode 3 | *.egg 4 | *.egg* 5 | *.pyc 6 | pycache 7 | *wheel* 8 | 9 | # Test directories 10 | *dummy* 11 | ignore* 12 | 13 | # Byte-compiled / optimized / DLL files 14 | __pycache__/ 15 | *.py[cod] 16 | *$py.class 17 | 18 | # C extensions 19 | *.so 20 | 21 | # Distribution / packaging 22 | .Python 23 | build/ 24 | develop-eggs/ 25 | dist/ 26 | downloads/ 27 | eggs/ 28 | .eggs/ 29 | lib/ 30 | lib64/ 31 | parts/ 32 | sdist/ 33 | var/ 34 | wheels/ 35 | *.egg-info/ 36 | .installed.cfg 37 | *.egg 38 | MANIFEST 39 | 40 | # PyInstaller 41 | # Usually these files are written by a python script from a template 42 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 43 | *.manifest 44 | *.spec 45 | 46 | # Installer logs 47 | pip-log.txt 48 | pip-delete-this-directory.txt 49 | 50 | # Unit test / coverage reports 51 | htmlcov/ 52 | .tox/ 53 | .coverage 54 | .coverage.* 55 | .cache 56 | nosetests.xml 57 | coverage.xml 58 | *.cover 59 | .hypothesis/ 60 | .pytest_cache/ 61 | 62 | # Translations 63 | *.mo 64 | *.pot 65 | 66 | # Django stuff: 67 | *.log 68 | local_settings.py 69 | *.sqlite3 70 | credentials.py 71 | 72 | # Keys + Confidential 73 | credentials* 74 | keys* 75 | *pipelines.yml 76 | .d_config/ 77 | 78 | # Flask stuff: 79 | instance/ 80 | .webassets-cache 81 | 82 | # Scrapy stuff: 83 | .scrapy 84 | 85 | # Sphinx documentation 86 | docs/_build/ 87 | 88 | # PyBuilder 89 | target/ 90 | 91 | # Jupyter Notebook 92 | .ipynb_checkpoints 93 | 94 | # pyenv 95 | .python-version 96 | 97 | # celery beat schedule file 98 | celerybeat-schedule 99 | 100 | # SageMath parsed files 101 | *.sage.py 102 | 103 | # Environments 104 | .env 105 | .env-dokku 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 | -------------------------------------------------------------------------------- /src/django_clite/template_files/app/init.tpl: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ 3 | {{ app }} 4 | """ 5 | from __future__ import unicode_literals 6 | 7 | __version__ = '{{ version }}' 8 | __license__ = '{{ license }}' 9 | __copyright__ = '{{ copyright }}' 10 | 11 | VERSION = __version__ 12 | 13 | default_app_config = '{{ app }}.apps.{{ classname }}Config' 14 | -------------------------------------------------------------------------------- /src/django_clite/template_files/app/pyproject.tpl: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "{{ app }}" 3 | description = "{{ description }}" 4 | authors = [{ name = "Your name", email = "email@example.com" }] 5 | maintainers = [{ name = "Your name", email = "email@example.com" }] 6 | requires-python = ">=3.8" 7 | license = { file = "LICENSE" } 8 | readme = "README.md" 9 | version = "0.1.0" 10 | keywords = [] 11 | classifiers = [] 12 | dependencies = [] 13 | 14 | [project.urls] 15 | Homepage = "https://github.com/username/repository" 16 | Issues = "https://github.com/username/repository/issues" 17 | -------------------------------------------------------------------------------- /src/django_clite/template_files/app/router.tpl: -------------------------------------------------------------------------------- 1 | from rest_framework import routers 2 | 3 | router = routers.SimpleRouter(trailing_slash=False) 4 | -------------------------------------------------------------------------------- /src/django_clite/template_files/app/router_init.tpl: -------------------------------------------------------------------------------- 1 | from .urls import router 2 | from .urls import urlpatterns 3 | -------------------------------------------------------------------------------- /src/django_clite/template_files/app/router_urls.tpl: -------------------------------------------------------------------------------- 1 | # {{ project }}:{{ app }}:api 2 | from django.urls import include, path 3 | from rest_framework import routers 4 | 5 | 6 | app_name = "{{ app }}" 7 | 8 | router = routers.SimpleRouter(trailing_slash=False) 9 | 10 | urlpatterns = [ 11 | path('', include(router.urls)), 12 | ] 13 | -------------------------------------------------------------------------------- /src/django_clite/template_files/app/routes.tpl: -------------------------------------------------------------------------------- 1 | # {{ if project }}{{ project }}:{% endif %}{{ app }}:urls 2 | """ 3 | Import urlpatterns into each view module and append the path to the list: 4 | # {{ if project }}{{ project }}/{% endif %}{{ app }}/views.my_view.py 5 | from django.urls import path 6 | from {{ if project }}{{ project }}.{% endif %}{{ app }}.views.urls import urlpatterns 7 | 8 | def my_view(request): 9 | # code here... 10 | 11 | urlpatterns.append( 12 | path('', my_view, name='my-view') 13 | ) 14 | 15 | 16 | Append the whole list to your urlpatterns: 17 | # {{ project }}/{{ app }}/urls.py 18 | from {{ if project }}{{ project }}.{% endif %}{{ app }}.views.urls import urlpatterns 19 | """ 20 | 21 | urlpatterns = [] 22 | -------------------------------------------------------------------------------- /src/django_clite/template_files/app/urls.tpl: -------------------------------------------------------------------------------- 1 | # {% if project %}{{ project }}:{% endif %}{{ app }}:urls 2 | 3 | """ 4 | To make this app's urls available to the entire project, 5 | include the urls in the urlpatterns for your project: 6 | 7 | In your project's urls.py file (the one close to wsgi.py or settings.py) do so: 8 | 9 | from django.urls import include, path 10 | urlpatterns = [ 11 | # Your other url patterns... 12 | path('{{ app }}/', include('{% if project %}{{ project }}.{% endif %}{{ app }}.urls', namespace='{{ app }}_urls')) 13 | ] 14 | """ 15 | 16 | from django.urls import include, path 17 | from .router.urls import urlpatterns as urls 18 | 19 | app_name = "{{ app }}" 20 | 21 | urlpatterns = [] + urls 22 | -------------------------------------------------------------------------------- /src/django_clite/template_files/docker/docker-compose.tpl: -------------------------------------------------------------------------------- 1 | {% if 'database' in services %} 2 | volumes: 3 | database: 4 | {% endif %} 5 | 6 | services: 7 | app: 8 | container_name: "{{ project }}_web" 9 | labels: 10 | com.{{ project }}.web.description: "{{ project }}: Web Application" 11 | build: 12 | context: . 13 | volumes: 14 | - .:/app 15 | env_file: 16 | - .env 17 | environment: 18 | DJANGO_ENV: docker 19 | entrypoint: /docker-entrypoint.sh 20 | command: "gunicorn {{ project }}.wsgi:application --bind 0.0.0.0:{{ port }} --workers {{ workers }}" 21 | ports: 22 | - 8007:{{ port }} # host:docker 23 | {%- if services %} 24 | depends_on: 25 | {%- for service in services %} 26 | - {{ service }} 27 | {% endfor %} 28 | {% endif %} 29 | 30 | 31 | {%- if 'database' in services %} 32 | database: 33 | container_name: "{{ project }}_database" 34 | image: postgres:16-alpine 35 | labels: 36 | com.database.description: "{{ project }}: Database service" 37 | volumes: 38 | - ./database:/var/lib/postgresql/data/ 39 | ports: 40 | - 5437:5432 # host:docker 41 | healthcheck: 42 | test: ["CMD-SHELL", "pg_ready -U postgres"] 43 | interval: 10s 44 | timeout: 83s 45 | retries: 40 46 | restart: always 47 | {% endif %} 48 | 49 | {%- if 'redis' in services %} 50 | redis: 51 | container_name: "{{ project }}_redis" 52 | image: redis:6.2.7 53 | labels: 54 | com.redis.description: "{{ project }}: Redis cache service" 55 | ports: 56 | - 6377:6379 # host:docker 57 | restart: always 58 | {% endif %} 59 | 60 | {%- if 'celery' in services %} 61 | celery_worker: 62 | container_name: "{{ project }}_celery_worker" 63 | labels: 64 | com.celery.description: "{{ project }}: Celery Worker" 65 | build: 66 | context: . 67 | command: celery worker --app {{ project }} --concurrency=20 -linfo -E 68 | depends_on: 69 | - redis 70 | env_file: 71 | - .env 72 | restart: on-failure 73 | stop_grace_period: 5s 74 | {% endif %} 75 | 76 | 77 | # These settings are provided for development purposes only. Not suitable for production. 78 | -------------------------------------------------------------------------------- /src/django_clite/template_files/docker/docker-entrypoint.tpl: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Collect staticfiles 4 | python3 manage.py collectstatic --noinput 5 | 6 | # Apply database migrations 7 | python3 manage.py migrate 8 | 9 | # Seed database 10 | # python3 manage.py loaddata /path/to/fixtures 11 | 12 | exec "$@" 13 | -------------------------------------------------------------------------------- /src/django_clite/template_files/docker/dockerfile.tpl: -------------------------------------------------------------------------------- 1 | # Base image 2 | FROM python:3.9-slim-stretch 3 | 4 | # Working directory 5 | WORKDIR /app 6 | 7 | # Environment variables 8 | ENV PYTHONDONTWRITEBYTECODE 1 9 | ENV PYTHONUNBUFFERED 1 10 | ENV LANG C.UTF-8 11 | 12 | # Copy dependencies 13 | # COPY Pipfile Pipfile 14 | # COPY Pipfile.lock Pipfile.lock 15 | COPY requirements.txt /app 16 | COPY docker-entrypoint.sh /docker-entrypoint.sh 17 | 18 | # Install dependencies 19 | RUN apt-get update \ 20 | && apt-get install -y \ 21 | swig libssl-dev dpkg-dev \ 22 | && pip install -U pip pipenv gunicorn \ 23 | # && pipenv lock --requirements > requirements.txt \ 24 | && pip install -r requirements.txt \ 25 | && chmod +x /docker-entrypoint.sh 26 | 27 | 28 | # Copy other files to docker container 29 | COPY . . 30 | 31 | # Switch users 32 | RUN groupadd -r docker && useradd --no-log-init -r -g docker docker 33 | USER docker 34 | 35 | CMD ["python3", "manage.py", "runserver"] 36 | -------------------------------------------------------------------------------- /src/django_clite/template_files/docker/dockerignore.tpl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oleoneto/django-clite/c47ff018a3ff4fcc202029805fb58bc4f999fda8/src/django_clite/template_files/docker/dockerignore.tpl -------------------------------------------------------------------------------- /src/django_clite/template_files/fixture.tpl: -------------------------------------------------------------------------------- 1 | [ 2 | {% for i in range(total) -%} 3 | { 4 | "pk": {{ loop.index }}, 5 | "model": "{{ app }}.{{ classname }}", 6 | "fields": { 7 | {% for attr_name, f in fields.items() -%} 8 | "{{ attr_name }}": "{{ f.example_value }}"{% if loop.index < loop.length %}, {%- endif -%} 9 | {% endfor %} 10 | } 11 | }{% if loop.index < loop.length %},{% endif -%} 12 | {% endfor %} 13 | ] 14 | -------------------------------------------------------------------------------- /src/django_clite/template_files/form.tpl: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from ..models.{{ name }} import {{ classname }} 3 | 4 | 5 | class {{ classname }}Form(forms.ModelForm): 6 | class Meta: 7 | model = {{ classname }} 8 | fields = "__all__" 9 | -------------------------------------------------------------------------------- /src/django_clite/template_files/github/ci.tpl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oleoneto/django-clite/c47ff018a3ff4fcc202029805fb58bc4f999fda8/src/django_clite/template_files/github/ci.tpl -------------------------------------------------------------------------------- /src/django_clite/template_files/github/pull_request_template.tpl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oleoneto/django-clite/c47ff018a3ff4fcc202029805fb58bc4f999fda8/src/django_clite/template_files/github/pull_request_template.tpl -------------------------------------------------------------------------------- /src/django_clite/template_files/kubernetes/configmap.tpl: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: {{ project }} 5 | data: 6 | DEBUG: "false" 7 | -------------------------------------------------------------------------------- /src/django_clite/template_files/kubernetes/deployment.tpl: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: &app {{ project }} 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: *app 10 | template: 11 | metadata: 12 | name: *app 13 | labels: 14 | app: *app 15 | spec: 16 | restartPolicy: Always 17 | containers: 18 | - name: *app 19 | image: &image "docker.io/{{ project }}" # modify this value 20 | imagePullPolicy: IfNotPresent 21 | securityContext: 22 | allowPrivilegeEscalation: false 23 | resources: 24 | limits: 25 | memory: "128Mi" 26 | ports: 27 | - name: &port http 28 | containerPort: 8080 29 | readinessProbe: 30 | httpGet: 31 | port: *port 32 | path: /robots.txt 33 | timeoutSeconds: 120 34 | livenessProbe: 35 | exec: 36 | command: 37 | - cat 38 | - /app/README.md 39 | timeoutSeconds: 30 40 | envFrom: 41 | - secretRef: 42 | name: *app 43 | - configMapRef: 44 | name: *app 45 | initContainers: 46 | - name: migrations 47 | image: *image 48 | securityContext: 49 | allowPrivilegeEscalation: false 50 | command: ["manage.py"] 51 | args: ["migrate"] 52 | envFrom: 53 | - secretRef: 54 | name: *app 55 | - configMapRef: 56 | name: *app 57 | - name: staticfiles 58 | image: *image 59 | securityContext: 60 | allowPrivilegeEscalation: false 61 | command: ["manage.py"] 62 | args: ["collectstatic", "--noinput"] 63 | envFrom: 64 | - secretRef: 65 | name: *app 66 | - configMapRef: 67 | name: *app 68 | -------------------------------------------------------------------------------- /src/django_clite/template_files/kubernetes/ingress.tpl: -------------------------------------------------------------------------------- 1 | # Example ingress definition. Requires Cert Manager to have already been configured. 2 | apiVersion: networking.k8s.io/v1 3 | kind: Ingress 4 | metadata: 5 | name: &app {{ project }} 6 | annotations: 7 | cert-manager.io/issuer: letsencrypt 8 | spec: 9 | # ingressClassName: nginx 10 | tls: 11 | - hosts: 12 | - example.com # modify this value 13 | secretName: "letsencrypt-{{ project }}" 14 | rules: 15 | - host: example.com # modify this value 16 | http: 17 | paths: 18 | - pathType: Prefix 19 | path: / 20 | backend: 21 | service: 22 | name: *app 23 | port: 24 | name: http 25 | -------------------------------------------------------------------------------- /src/django_clite/template_files/kubernetes/service.tpl: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: &app {{ project }} 5 | spec: 6 | ports: 7 | - name: http 8 | port: 3000 9 | targetPort: http 10 | selector: 11 | app: *app 12 | -------------------------------------------------------------------------------- /src/django_clite/template_files/management.tpl: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | from django.db.utils import IntegrityError 3 | # from ..models import MyModel 4 | 5 | 6 | class Command(BaseCommand): 7 | help = "What this command does" 8 | 9 | def add_arguments(self, parser): 10 | pass 11 | 12 | def handle(self, *args, **options): 13 | """ 14 | Example: 15 | 16 | try: 17 | [MyModel.objects.create(**i) for i in [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]] 18 | except IntegrityError: 19 | print(f'Skipping creation of record {i} it seems to already exist.') 20 | """ 21 | 22 | pass 23 | -------------------------------------------------------------------------------- /src/django_clite/template_files/models/manager.tpl: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.db.models import Q 3 | 4 | 5 | class {{ classname }}Manager(models.Manager): 6 | """ 7 | Applying a custom QuerySet. 8 | def get_queryset(self): 9 | return Custom{{ classname }}QuerySet(self.model, using=self._db) 10 | 11 | 12 | In your desired model, assign the manager to one of your model attributes. 13 | objects = {{ classname }}Manager() 14 | 15 | If you want to create fixtures by passing ['field1', 'field2']: 16 | def get_by_natural_key(self, field1, field2): 17 | return self.get(field1=field1, field2=field2) 18 | """ 19 | 20 | def get_queryset(self): 21 | return super().get_queryset().prefetch_related() 22 | 23 | def get_for_user(self, user): 24 | return self.get_queryset().filter(user=user) 25 | -------------------------------------------------------------------------------- /src/django_clite/template_files/models/model.tpl: -------------------------------------------------------------------------------- 1 | import uuid 2 | from django.db import models 3 | from django.urls import reverse 4 | from django.utils.translation import gettext_lazy as _ 5 | from django.template.defaultfilters import slugify 6 | {% for attr_name, field in imports.items() -%} 7 | from ..models.{{ field.module_name(attr_name) }} import {{ field.klass_name(attr_name) }} 8 | {% endfor %} 9 | 10 | class {{ classname }}(models.Model): 11 | {% for attr_name, f in fields.items() -%} 12 | {{attr_name}} = models.{{f.kind}}({{ f.field_options(attr_name, classname) }}) 13 | {% endfor %} 14 | # Default fields. Used for record-keeping. 15 | uuid = models.UUIDField(default=uuid.uuid4, editable=False) 16 | created_at = models.DateTimeField(_('created at'), auto_now_add=True, editable=False) 17 | updated_at = models.DateTimeField(_('updated at'), auto_now=True, editable=False) 18 | 19 | class Meta: 20 | {%- if abstract %} 21 | abstract = True{%- endif %} 22 | db_table = '{{ table_name }}' 23 | indexes = [models.Index(fields=['created_at'])] 24 | ordering = ['-created_at'] 25 | 26 | @property 27 | def slug(self): 28 | # Generate a Medium/Notion-like URL slugs: 29 | return slugify(f'{str(self.uuid)[-12:]}') 30 | 31 | def get_absolute_url(self): 32 | return reverse('{{ name.lower() }}-detail', kwargs={'slug': self.slug}) 33 | 34 | def __str__(self): 35 | return f'{self.slug}' 36 | -------------------------------------------------------------------------------- /src/django_clite/template_files/models/signal.tpl: -------------------------------------------------------------------------------- 1 | from django.dispatch import receiver 2 | from django.utils.translation import gettext_lazy as _ 3 | from django.template.loader import get_template 4 | from django.template import Context 5 | from django.db.models.signals import pre_save, post_save, post_delete 6 | # from ..models import MyModel 7 | 8 | 9 | # @receiver(post_save, sender=MyModel, dispatch_uid="{{ name }}") 10 | def {{ name }}(sender, **kwargs): 11 | instance = kwargs.get('instance') 12 | # template = get_template('template.txt') 13 | # code here... 14 | -------------------------------------------------------------------------------- /src/django_clite/template_files/models/test.tpl: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | 4 | class {{ classname }}TestCase(TestCase): 5 | def setUp(self): 6 | """ 7 | Create sample objects 8 | 9 | Example: 10 | {{ classname }}.objects.create() 11 | """ 12 | 13 | pass 14 | 15 | def test_create_record(self): 16 | """ 17 | Run assertions 18 | 19 | Example: 20 | record = {{ classname }}.objects.create() 21 | self.assertEqual(record.id, 1) 22 | """ 23 | 24 | pass 25 | -------------------------------------------------------------------------------- /src/django_clite/template_files/models/validator.tpl: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import ValidationError 2 | 3 | 4 | def {{ name }}_validator(value): 5 | pass 6 | -------------------------------------------------------------------------------- /src/django_clite/template_files/project/README.tpl: -------------------------------------------------------------------------------- 1 | # {{ project }} 2 | {% if author %}Author: {{ author }}{% endif %} 3 | 4 | #### Requirements 5 | [Requirements](requirements.txt) 6 | 7 | ---- 8 | 9 | #### Pipenv and dependencies 10 | Run `pipenv install --deploy` to install all dependencies listed in the Pipfile. 11 | 12 | {% if custom_auth %} 13 | ---- 14 | ### Authentication with custom `AUTH_USER_MODEL` 15 | Authentication is overridden in favor of [`authentication.User`]({{ project }}/authentication/models/user.py). 16 | If you haven't already, add the following line to your project's [settings.py]({{ project }}/settings.py): 17 | ```python 18 | AUTH_USER_MODEL = 'authentication.User' @{{ project }}.{{ author }} 19 | ``` 20 | 21 | When running migrations for your project for the first time, remember to follow the following workflow: 22 | ``` 23 | python manage.py makemigrations authentication && \ 24 | python manage.py migrate authentication && \ 25 | python manage.py migrate 26 | ``` 27 | 28 | This ensures the `AUTH_USER_MODEL` is set to `authentication.User`. 29 | It is important to follow the order above because of how django sets up the database. 30 | {% endif %} 31 | 32 | ### Create superuser 33 | ``` 34 | python manage.py createsuperuser 35 | ``` 36 | 37 | ---- 38 | 39 | ### Environment variables 40 | For safety reasons, prefer to use environment variables instead of hard-coding sensitive values. 41 | This project has two environment files [`.env`](.env) and [`.env-example`](.env-example) which you can use to 42 | manage your application configuration and secrets. 43 | 44 | Your actual secrets should live in `.env`. This file should not be committed to your repository, but should be added to 45 | [`.gitignore`](.gitignore). 46 | Use `.env-example` to specify the keys that must be set in order for your application to run once deployed. 47 | 48 | 49 | --- 50 | 51 | ### To Do 52 | [Check out our open issues](/issues) 53 | 54 | --- 55 | 56 | ### Pull requests 57 | Found a bug or have a feature request? Open an issue or submit a PR. 58 | 59 | --- 60 | 61 | ### LICENSE 62 | **{{ project }}**. [Check out the license](LICENSE). 63 | 64 | ---- 65 | 66 | Project generated with [django-clite](https://github.com/oleoneto/django-clite) 67 | -------------------------------------------------------------------------------- /src/django_clite/template_files/project/__init__.py: -------------------------------------------------------------------------------- 1 | # django_clite:templates:project 2 | -------------------------------------------------------------------------------- /src/django_clite/template_files/project/api.tpl: -------------------------------------------------------------------------------- 1 | # api settings go here 2 | # {{ project }}->{{ name }} 3 | -------------------------------------------------------------------------------- /src/django_clite/template_files/project/app_json.tpl: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "dokku": { 4 | "predeploy": "/app/manage.py migrate --noinput" 5 | } 6 | } 7 | } -------------------------------------------------------------------------------- /src/django_clite/template_files/project/celery.tpl: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | import os 4 | 5 | from celery import Celery 6 | 7 | # set the default Django settings module for the 'celery' program. 8 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', '{{ project }}.settings') 9 | 10 | app = Celery('{{ project }}') 11 | 12 | # Using a string here means the worker doesn't have to serialize 13 | # the configuration object to child processes. 14 | # - namespace='CELERY' means all celery-related configuration keys 15 | # should have a `CELERY_` prefix. 16 | app.config_from_object('django.conf:settings', namespace='CELERY') 17 | 18 | # Load task modules from all registered Django app configs. 19 | app.autodiscover_tasks() 20 | 21 | 22 | @app.task(bind=True) 23 | def debug_task(self): 24 | print('Request: {0!r}'.format(self.request)) 25 | -------------------------------------------------------------------------------- /src/django_clite/template_files/project/celery_init.tpl: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | # This will make sure the app is always imported when 4 | # Django starts so that shared_task will use this app. 5 | from .celery import app as celery_app 6 | 7 | __all__ = ('celery_app',) 8 | -------------------------------------------------------------------------------- /src/django_clite/template_files/project/celery_tasks.tpl: -------------------------------------------------------------------------------- 1 | from celery import shared_task 2 | 3 | 4 | @shared_task 5 | def my_{{ project }}_task(data=None): 6 | # This is a sample celery task 7 | pass 8 | -------------------------------------------------------------------------------- /src/django_clite/template_files/project/cli-config_json.tpl: -------------------------------------------------------------------------------- 1 | { 2 | "project": { 3 | "name": "{{ project }}", 4 | "path": "{{ path }}", 5 | "created_at": "{{ created_at }}", 6 | "updated_at": "{{ updated_at }}", 7 | }, 8 | "apps": [ 9 | {% for app in apps %} 10 | { 11 | "{{ app.name }}": { 12 | "path": "{{ app['path'] }}", 13 | "models": [] 14 | } 15 | }, 16 | {% endfor %} 17 | ] 18 | } -------------------------------------------------------------------------------- /src/django_clite/template_files/project/cli-config_yaml.tpl: -------------------------------------------------------------------------------- 1 | project: {{ project }} 2 | {% if apps %}apps:{% for app in apps %} 3 | - {{ app }} 4 | {% endfor %}{% endif %} 5 | -------------------------------------------------------------------------------- /src/django_clite/template_files/project/dokku_checks.tpl: -------------------------------------------------------------------------------- 1 | DOKKU_CHECKS_WAIT={{ wait }} 2 | DOKKU_CHECKS_TIMEOUT={{ timeout }} -------------------------------------------------------------------------------- /src/django_clite/template_files/project/dokku_scale.tpl: -------------------------------------------------------------------------------- 1 | web={{ web }} -------------------------------------------------------------------------------- /src/django_clite/template_files/project/env.tpl: -------------------------------------------------------------------------------- 1 | # Example variables that could be in an environment file. 2 | # Commit only the .env-example file, not the .env file. Use the example file to illustrate 3 | # what variables your environment uses, but be sure to not commit this file with any sensitive information. 4 | 5 | DEBUG=True 6 | 7 | ADMINS=# i.e (('Admin', 'admin@example.com'),) 8 | 9 | SECRET_KEY= 10 | 11 | SERVER_EMAIL=# i.e no-reply@example.com 12 | EMAIL_HOST= 13 | EMAIL_HOST_USER= 14 | EMAIL_HOST_PASSWORD= 15 | 16 | REDIS_URL=# i.e. redis://[:password]@127.0.0.1:6379 17 | 18 | DATABASE_URL=postgres://u:p@service:5432/service 19 | -------------------------------------------------------------------------------- /src/django_clite/template_files/project/github_ci.tpl: -------------------------------------------------------------------------------- 1 | name: Django CI 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | push: 7 | branches: [main] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | strategy: 14 | max-parallel: 4 15 | matrix: 16 | python-version: [3.7, 3.8, 3.9] 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Set up Python {% raw %}${{ matrix.python-version }}{% endraw %} 21 | uses: actions/setup-python@v2 22 | with: 23 | python-version: {% raw %}${{ matrix.python-version }}{% endraw %} 24 | - name: Install Dependencies 25 | run: | 26 | python -m pip install --upgrade pip 27 | pip install -r requirements.txt 28 | - name: Run Tests 29 | run: | 30 | python manage.py test 31 | -------------------------------------------------------------------------------- /src/django_clite/template_files/project/gitignore.tpl: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode 3 | *.egg 4 | *.egg* 5 | *.pyc 6 | pycache 7 | *wheel* 8 | 9 | # Test directories 10 | *dummy* 11 | ignore* 12 | 13 | # Byte-compiled / optimized / DLL files 14 | __pycache__/ 15 | *.py[cod] 16 | *$py.class 17 | 18 | # C extensions 19 | *.so 20 | 21 | # Distribution / packaging 22 | .Python 23 | build/ 24 | develop-eggs/ 25 | dist/ 26 | downloads/ 27 | eggs/ 28 | .eggs/ 29 | lib/ 30 | lib64/ 31 | parts/ 32 | sdist/ 33 | var/ 34 | wheels/ 35 | *.egg-info/ 36 | .installed.cfg 37 | *.egg 38 | MANIFEST 39 | 40 | # PyInstaller 41 | # Usually these files are written by a python script from a template 42 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 43 | *.manifest 44 | *.spec 45 | 46 | # Installer logs 47 | pip-log.txt 48 | pip-delete-this-directory.txt 49 | 50 | # Unit test / coverage reports 51 | htmlcov/ 52 | .tox/ 53 | .coverage 54 | .coverage.* 55 | .cache 56 | nosetests.xml 57 | coverage.xml 58 | *.cover 59 | .hypothesis/ 60 | .pytest_cache/ 61 | 62 | # Translations 63 | *.mo 64 | *.pot 65 | 66 | # Django stuff: 67 | *.log 68 | local_settings.py 69 | *.sqlite3 70 | credentials.py 71 | 72 | # Keys + Confidential 73 | credentials* 74 | keys* 75 | *pipelines.yml 76 | .d_config/ 77 | 78 | # Flask stuff: 79 | instance/ 80 | .webassets-cache 81 | 82 | # Scrapy stuff: 83 | .scrapy 84 | 85 | # Sphinx documentation 86 | docs/_build/ 87 | 88 | # PyBuilder 89 | target/ 90 | 91 | # Jupyter Notebook 92 | .ipynb_checkpoints 93 | 94 | # pyenv 95 | .python-version 96 | 97 | # celery beat schedule file 98 | celerybeat-schedule 99 | 100 | # SageMath parsed files 101 | *.sage.py 102 | 103 | # Environments 104 | .env 105 | .env-dokku 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 | -------------------------------------------------------------------------------- /src/django_clite/template_files/project/pipfile.tpl: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | boto3 = "==1.13.18" 10 | channels = "==2.4.0" 11 | cloudinary = "==1.20.0" 12 | coreapi = "==2.3.3" 13 | coverage = "*" 14 | django = "==2.2.4" 15 | django-allauth = "==0.42.0" 16 | django-celery-beat = "==2.0.0" 17 | django-compress = "*" 18 | django-compressor = "*" 19 | django-cors-headers = "==3.3.0" 20 | django-debug-toolbar = "==2.2" 21 | django-environ = "==0.4.5" 22 | django-extensions = "==2.2.9" 23 | django-filter = "==2.0.0" 24 | django-guardian = "==2.2.0" 25 | django-hijack = "==2.1.10" 26 | django-livereload-server = "==0.3.2" 27 | django-oauth-plus = "*" 28 | django-oauth2-provider = "*" 29 | django-otp = "==0.9.1" 30 | django-redis = "==4.12.1" 31 | django-role-permissions = "==3.0.0" 32 | django-storages = "==1.9.1" 33 | django-tenant-schemas = "*" 34 | django-widget-tweaks = "==1.4.8" 35 | django_polymorphic = "==2.1.2" 36 | djangorestframework = ">=3.9.1" 37 | djangorestframework-httpsignature = "==1.0.0" 38 | djangorestframework-jsonapi = "==3.1.0" 39 | djangorestframework-simplejwt = "==4.4.0" 40 | djongo = "==1.3.2" 41 | drf-dynamic-fields = "==0.3.1" 42 | drf-flex-fields = "==0.8.5" 43 | drf-nested-routers = "==0.91" 44 | gunicorn = "==20.0.4" 45 | health-check = "*" 46 | jupyterlab = "*" 47 | oauth2 = "*" 48 | Pillow = "==7.1.2" 49 | psycopg2-binary = "==2.8.5" 50 | python-coveralls = "*" 51 | python-dotenv = "*" 52 | qrcode = "==6.1" 53 | sentry-sdk = "==0.14.4" 54 | stripe = "==2.48.0" 55 | twilio = "==6.41.0" 56 | werkzeug = "==1.0.1" 57 | 58 | 59 | [requires] 60 | python_version = "3.7" 61 | -------------------------------------------------------------------------------- /src/django_clite/template_files/project/procfile.tpl: -------------------------------------------------------------------------------- 1 | web: gunicorn {{ project }}.wsgi -------------------------------------------------------------------------------- /src/django_clite/template_files/project/requirements.tpl: -------------------------------------------------------------------------------- 1 | django>=4.2 2 | gunicorn>=21 3 | Pillow>=10 4 | psycopg2-binary>=2.9.10 5 | python-dotenv>=1 6 | werkzeug>=3.0 7 | 8 | django-debug-toolbar>=5.0 9 | django-extensions>=3 10 | django-livereload-server>=0.5 11 | 12 | ## Authentication 13 | django-allauth>=65 14 | 15 | ## API 16 | django-cors-headers>=4 17 | djangorestframework>=3.15 18 | 19 | ## Tasks 20 | celery>=5 21 | django-celery-beat>=2.7 22 | 23 | ## Tests 24 | coverage 25 | -------------------------------------------------------------------------------- /src/django_clite/template_files/project/settings.tpl: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for {{ project }} project. 3 | """ 4 | 5 | import os 6 | import re 7 | from dotenv import load_dotenv 8 | 9 | # Read values from system environment 10 | load_dotenv() 11 | 12 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 13 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 14 | 15 | 16 | # Quick-start development settings - unsuitable for production 17 | 18 | # SECURITY WARNING: keep the secret key used in production secret! 19 | SECRET_KEY = os.environ.get('SECRET_KEY') 20 | 21 | # SECURITY WARNING: don't run with debug turned on in production! 22 | DEBUG = bool(os.environ.get('DEBUG')) 23 | 24 | ALLOWED_HOSTS = {% if domain %}['{{ domain }}']{% else %}[]{% endif %} 25 | 26 | INTERNAL_IPS = '127.0.0.1' 27 | 28 | 29 | # Stripe account information. Use TEST values in DEBUG mode. 30 | 31 | STRIPE_LIVE_MODE = False if DEBUG else bool(os.environ.get('STRIPE_LIVE_MODE')) 32 | 33 | STRIPE_TEST_SECRET_KEY = os.environ.get('STRIPE_TEST_SECRET_KEY') 34 | 35 | STRIPE_TEST_PUBLIC_KEY = os.environ.get('STRIPE_TEST_PUBLISHABLE_KEY') 36 | 37 | STRIPE_SECRET_KEY = os.environ.get('STRIPE_TEST_SECRET_KEY') \ 38 | if DEBUG else os.environ.get('STRIPE_LIVE_SECRET_KEY') 39 | 40 | STRIPE_PUBLIC_KEY = os.environ.get('STRIPE_TEST_PUBLISHABLE_KEY') \ 41 | if DEBUG else os.environ.get('STRIPE_LIVE_PUBLISHABLE_KEY') 42 | 43 | 44 | # Application definition 45 | 46 | SITE_ID = 1 47 | 48 | INSTALLED_APPS = [ 49 | 'django.contrib.admin', 50 | 'django.contrib.auth', 51 | 'django.contrib.contenttypes', 52 | 'django.contrib.sessions', 53 | 'django.contrib.messages', 54 | 'django.contrib.staticfiles', 55 | 'django.contrib.sites', 56 | 57 | {% if installable_apps %}# Apps installed from cli{% for app in installable_apps %} 58 | '{{ app }}',{% endfor %}{% endif %} 59 | {% if apps %}# {{ project }} apps{% for app in apps %} 60 | '{{ project }}.{{ app }}.apps.{{ app.capitalize() }}Config',{% endfor %}{% endif %} 61 | ] 62 | 63 | MIDDLEWARE = [ 64 | 'django.contrib.sites.middleware.CurrentSiteMiddleware', 65 | 'django.middleware.security.SecurityMiddleware', 66 | 'django.contrib.sessions.middleware.SessionMiddleware', 67 | # 'django.middleware.cache.UpdateCacheMiddleware', # <-- Caching 68 | 'django.middleware.common.CommonMiddleware', 69 | # 'django.middleware.cache.FetchFromCacheMiddleware', # <-- Caching 70 | 'django.middleware.csrf.CsrfViewMiddleware', 71 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 72 | 'django.contrib.messages.middleware.MessageMiddleware', 73 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 74 | {% if installable_middleware %}# Middleware installed with django-clite{% for m in installable_middleware %} 75 | '{{ m }}',{% endfor %}{% endif %} 76 | ] 77 | 78 | ROOT_URLCONF = '{{ project }}.urls' 79 | 80 | TEMPLATES = [ 81 | { 82 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 83 | 'DIRS': [{% if apps %}{% for app in apps %} 84 | os.path.join(BASE_DIR, '{{ project }}/{{ app }}/templates'),{% endfor %}{% endif %} 85 | ], 86 | 'APP_DIRS': True, 87 | 'OPTIONS': { 88 | 'context_processors': [ 89 | 'django.template.context_processors.debug', 90 | 'django.template.context_processors.request', 91 | 'django.contrib.auth.context_processors.auth', 92 | 'django.contrib.messages.context_processors.messages', 93 | ], 94 | }, 95 | }, 96 | ] 97 | 98 | WSGI_APPLICATION = '{{ project }}.wsgi.application' 99 | 100 | 101 | # Database 102 | 103 | ENGINE = 'django.db.backends.postgresql' 104 | 105 | if "DATABASE_URL" in os.environ: 106 | 107 | USER, PASSWORD, HOST, PORT, NAME = re.match("^postgres://(?P.*?)\:(?P.*?)\@(?P.*?)\:(?P\d+)\/(?P.*?)$", os.environ.get("DATABASE_URL", "")).groups() 108 | 109 | DATABASES = { 110 | 'default': { 111 | 'ENGINE': ENGINE, 112 | 'NAME': NAME, 113 | 'USER': USER, 114 | 'PASSWORD': PASSWORD, 115 | 'HOST': HOST, 116 | 'PORT': int(PORT), 117 | } 118 | } 119 | 120 | else: 121 | 122 | DATABASES = { 123 | 'default': { 124 | 'ENGINE': ENGINE, 125 | 'NAME': 'postgres', 126 | 'USER': 'postgres', 127 | 'HOST': 'db', 128 | 'PORT': 5432, 129 | } 130 | } 131 | 132 | CACHES = { 133 | "default": { 134 | "BACKEND": "django_redis.cache.RedisCache", 135 | "LOCATION": os.environ.get("REDIS_URL", "") + "/1", 136 | "OPTIONS": { 137 | "CLIENT_CLASS": "django_redis.client.DefaultClient" 138 | }, 139 | "KEY_PREFIX": "{{ project }}" 140 | } 141 | } 142 | 143 | CACHE_TTL = 60 * 15 144 | 145 | CELERY_BROKER_URL = os.environ.get("REDIS_URL", "") + "/1" 146 | 147 | CELERY_RESULT_BACKEND = os.environ.get("REDIS_URL", "") + "/1" 148 | 149 | CELERY_TASK_ALWAYS_EAGER = True 150 | 151 | # CELERY_ACCEPT_CONTENT = ['application/json'] 152 | 153 | # CELERY_TASK_SERIALIZER = 'json' 154 | 155 | # CELERY_RESULT_SERIALIZER = 'json' 156 | 157 | # Email server backend configuration 158 | # Use SendGrid to deliver automated email 159 | 160 | EMAIL_HOST = os.environ.get('EMAIL_HOST') 161 | EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER') 162 | EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD') 163 | EMAIL_PORT = 587 164 | EMAIL_USE_TLS = True 165 | EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" 166 | 167 | if DEBUG: 168 | EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" 169 | 170 | 171 | # Password validation 172 | 173 | AUTH_USER_MODEL = 'authentication.User' 174 | 175 | AUTHENTICATION_BACKENDS = [ 176 | 'django.contrib.auth.backends.ModelBackend', 177 | 'guardian.backends.ObjectPermissionBackend', 178 | 'allauth.account.auth_backends.AuthenticationBackend', 179 | ] 180 | 181 | AUTH_PASSWORD_VALIDATORS = [ 182 | { 183 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 184 | }, 185 | { 186 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 187 | }, 188 | { 189 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 190 | }, 191 | { 192 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 193 | }, 194 | ] 195 | 196 | # https://django-allauth.readthedocs.io/en/latest/providers.html 197 | 198 | # ACCOUNT_AUTHENTICATION_METHOD = 'email' 199 | 200 | ACCOUNT_USER_MODEL_USERNAME_FIELD = 'username' 201 | 202 | ACCOUNT_USERNAME_REQUIRED = True 203 | 204 | ACCOUNT_EMAIL_REQUIRED = True 205 | 206 | ACCOUNT_EMAIL_VERIFICATION = "mandatory" 207 | 208 | ACCOUNT_USERNAME_MIN_LENGTH = 6 209 | 210 | ACCOUNT_LOGIN_ATTEMPTS_LIMIT = int(os.environ.get('ACCOUNT_LOGIN_ATTEMPTS_LIMIT')) 211 | 212 | ACCOUNT_LOGIN_ATTEMPTS_TIMEOUT = int(os.environ.get('ACCOUNT_LOGIN_ATTEMPTS_TIMEOUT')) 213 | 214 | ACCOUNT_USERNAME_BLACKLIST = os.environ.get('ACCOUNT_USERNAME_BLACKLIST') 215 | 216 | ACCOUNT_LOGOUT_REDIRECT_URL = '/' 217 | 218 | LOGIN_REDIRECT_URL = '/accounts/me/' 219 | 220 | SOCIALACCOUNT_QUERY_EMAIL = True 221 | 222 | GUARDIAN_RAISE_403 = True 223 | 224 | OTP_TOTP_ISSUER = os.environ.get('OTP_ISSUER') 225 | 226 | 227 | # Internationalization 228 | 229 | LANGUAGE_CODE = 'en-us' 230 | 231 | TIME_ZONE = 'UTC' 232 | 233 | USE_I18N = True 234 | 235 | USE_L10N = True 236 | 237 | USE_TZ = True 238 | 239 | 240 | # Static files (CSS, JavaScript, Images) 241 | 242 | AWS_ACCESS_KEY_ID = os.environ.get('AWS_ACCESS_KEY_ID') 243 | 244 | AWS_SECRET_ACCESS_KEY = os.environ.get('AWS_SECRET_ACCESS_KEY') 245 | 246 | AWS_STORAGE_BUCKET_NAME = os.environ.get('AWS_BUCKET_NAME') 247 | 248 | AWS_S3_ENDPOINT_URL = os.environ.get('AWS_ENDPOINT_URL') 249 | 250 | AWS_S3_CUSTOM_DOMAIN = os.environ.get('AWS_CUSTOM_DOMAIN') 251 | 252 | AWS_LOCATION = os.environ.get('AWS_LOCATION') 253 | 254 | AWS_S3_OBJECT_PARAMETERS = { 255 | 'CacheControl': 'max-age=86400', 256 | } 257 | 258 | AWS_IS_GZIPPED = True 259 | 260 | DEFAULT_FILE_STORAGE = '{{ project }}.storage.PublicFileStorage' 261 | 262 | STATICFILES_STORAGE = '{{ project }}.storage.StaticStorage' 263 | 264 | STATICFILES_FINDERS = [ 265 | 'django.contrib.staticfiles.finders.FileSystemFinder', 266 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 267 | ] 268 | 269 | STATIC_URL = f'{AWS_S3_ENDPOINT_URL}/' 270 | 271 | STATIC_ROOT = 'staticfiles/' 272 | 273 | MEDIA_ROOT = 'mediafiles/' 274 | 275 | MEDIA_URL = '/files/' 276 | 277 | 278 | # Django REST Framework, JWT, and Swagger configurations go here. 279 | 280 | # REST_FRAMEWORK = {} 281 | 282 | # SIMPLE_JWT = {} 283 | 284 | # SWAGGER_SETTINGS = {} 285 | 286 | 287 | # Error logging and reporting with Sentry 288 | # Advanced error reporting in production. 289 | 290 | import sentry_sdk 291 | from sentry_sdk.integrations.django import DjangoIntegration 292 | from sentry_sdk.integrations.redis import RedisIntegration 293 | 294 | sentry_sdk.init( 295 | dsn=os.environ.get('SENTRY_DSN'), 296 | integrations=[ 297 | DjangoIntegration(), 298 | RedisIntegration() 299 | ] 300 | ) 301 | -------------------------------------------------------------------------------- /src/django_clite/template_files/project/storage.tpl: -------------------------------------------------------------------------------- 1 | from storages.backends.s3boto3 import S3Boto3Storage 2 | 3 | 4 | # Static assets: CSS, Javascript 5 | class StaticStorage(S3Boto3Storage): 6 | location = 'static' 7 | default_acl = 'public-read' 8 | file_overwrite = True 9 | 10 | 11 | class PrivateFileStorage(S3Boto3Storage): 12 | location = 'private-files' 13 | default_acl = 'private' 14 | file_overwrite = False 15 | custom_domain = False 16 | 17 | 18 | class PublicFileStorage(S3Boto3Storage): 19 | location = 'files' 20 | default_acl = 'public-read' 21 | file_overwrite = False 22 | -------------------------------------------------------------------------------- /src/django_clite/template_files/project/travis-yml.tpl: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.7" 4 | env: 5 | - DJANGO_VERSION=2.2.3 6 | branches: 7 | only: 8 | - master 9 | # Start services. 10 | services: 11 | - redis-server 12 | - postgresql 13 | # command to install dependencies 14 | install: 15 | - pip install -q Django==$DJANGO_VERSION 16 | - pip install -r requirements/local.txt 17 | - pip install coveralls 18 | before_script: 19 | - psql -c "CREATE DATABASE db;" -U postgres 20 | - cp .env.example .env 21 | - python manage.py migrate 22 | - python manage.py collectstatic --noinput 23 | # command to run tests 24 | script: 25 | - pytest 26 | after_success: 27 | - coveralls 28 | -------------------------------------------------------------------------------- /src/django_clite/template_files/project/urls.tpl: -------------------------------------------------------------------------------- 1 | from django.urls import path, include 2 | import rest_framework.authtoken.views as rf 3 | 4 | handler400 = 'rest_framework.exceptions.bad_request' 5 | handler500 = 'rest_framework.exceptions.server_error' 6 | 7 | urlpatterns = [ 8 | path('auth/token', rf.obtain_auth_token), 9 | path('auth/', include('rest_framework.urls', namespace='rest_framework')), 10 | # path('v1/', include('project.example.api', namespace='example_api')), 11 | ] -------------------------------------------------------------------------------- /src/django_clite/template_files/serializer.tpl: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from ..models import {{ classname }} 3 | 4 | 5 | class {{ classname }}Serializer(serializers.ModelSerializer): 6 | # Add related fields below: 7 | # Example relation fields are: 8 | # -- HyperlinkedIdentityField 9 | # -- HyperlinkedRelatedField 10 | # -- PrimaryKeyRelatedField 11 | # -- SlugRelatedField 12 | # -- StringRelatedField 13 | 14 | # You can also add a custom serializer, like so: 15 | # likes = LikeSerializer(many=True) 16 | 17 | class Meta: 18 | model = {{ classname }} 19 | fields = "__all__" 20 | -------------------------------------------------------------------------------- /src/django_clite/template_files/shared/init.tpl: -------------------------------------------------------------------------------- 1 | # your package imports go here 2 | -------------------------------------------------------------------------------- /src/django_clite/template_files/templates/create.tpl: -------------------------------------------------------------------------------- 1 | {%- raw -%} 2 | {% comment %} 3 | Describe the template here. 4 | {% endcomment %} 5 | 6 | {% block content %} 7 |
8 |

Create Form

9 | 10 |
11 | {{ form }} 12 |
13 |
14 | {% endblock %} 15 | {%- endraw %} 16 | -------------------------------------------------------------------------------- /src/django_clite/template_files/templates/detail.tpl: -------------------------------------------------------------------------------- 1 | {%- raw -%} 2 | {% comment %} 3 | Describe the template here. 4 | {% endcomment %} 5 | 6 | {% block content %} 7 |
8 |

{{ object }}

9 | 10 |
11 |
12 | {% endblock %} 13 | {%- endraw %} 14 | -------------------------------------------------------------------------------- /src/django_clite/template_files/templates/list.tpl: -------------------------------------------------------------------------------- 1 | {%- raw -%} 2 | {% comment %} 3 | Describe the template here. 4 | {% endcomment %} 5 | 6 | {% block content %} 7 |
8 |

ListView

9 |
10 | {% for object in object_list %} 11 |
12 |
13 |

14 | {{ object }} 15 |

16 |
17 |
18 | 19 |
20 | {% endfor %} 21 |
22 |
23 | {% endblock %} 24 | {%- endraw %} 25 | -------------------------------------------------------------------------------- /src/django_clite/template_files/templates/template.tpl: -------------------------------------------------------------------------------- 1 | {{ '{% load cache %}' }} 2 | 3 | {{ '{% comment %}' }} 4 | Describe the template here. 5 | {{ '{% endcomment %}' }} 6 | 7 | {{ '{% block content %}{% endblock content %}' }} 8 | -------------------------------------------------------------------------------- /src/django_clite/template_files/templates/update.tpl: -------------------------------------------------------------------------------- 1 | {%- raw -%} 2 | {% comment %} 3 | Describe the template here. 4 | {% endcomment %} 5 | 6 | {% block content %} 7 |
8 |

{{ object }}

9 | 10 |
11 | {{ form }} 12 |
13 |
14 | {% endblock %} 15 | {%- endraw %} 16 | -------------------------------------------------------------------------------- /src/django_clite/template_files/templatetags/tag.tpl: -------------------------------------------------------------------------------- 1 | from django import template 2 | 3 | register = template.Library() 4 | 5 | 6 | @register.filter 7 | def {{ name }}(queryset): 8 | pass 9 | -------------------------------------------------------------------------------- /src/django_clite/template_files/views/create.tpl: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from django.views.generic import CreateView 3 | from ..urls import urlpatterns 4 | from ..models import {{ classname }} 5 | from ..forms import {{ classname }}Form 6 | 7 | 8 | class {{ classname }}CreateView(CreateView): 9 | model = {{ classname }} 10 | form_class = {{ classname }}Form 11 | context_object_name = '{{ name }}' 12 | template_name = '{{ template_name }}' 13 | 14 | def get_context_data(self, **kwargs): 15 | context = super().get_context_data(**kwargs) 16 | return context 17 | 18 | 19 | urlpatterns.append( 20 | path('{{ namespace }}/new', {{ classname }}CreateView.as_view(), name='{{ name }}-create') 21 | ) 22 | -------------------------------------------------------------------------------- /src/django_clite/template_files/views/detail.tpl: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from django.views.generic import DetailView 3 | from ..urls import urlpatterns 4 | from ..models import {{ classname }} 5 | 6 | 7 | class {{ classname }}DetailView(DetailView): 8 | model = {{ classname }} 9 | context_object_name = '{{ name }}' 10 | template_name = '{{ template_name }}' 11 | 12 | def get_context_data(self, **kwargs): 13 | context = super().get_context_data(**kwargs) 14 | return context 15 | 16 | 17 | urlpatterns.append( 18 | path('{{ namespace }}/', {{ classname }}DetailView.as_view(), name='{{ name }}-detail') 19 | ) 20 | -------------------------------------------------------------------------------- /src/django_clite/template_files/views/list.tpl: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from django.views.generic import ListView 3 | from ..urls import urlpatterns 4 | from ..models import {{ classname }} 5 | 6 | 7 | class {{ classname }}ListView(ListView): 8 | model = {{ classname }} 9 | context_object_name = '{{ namespace }}' 10 | template_name = '{{ template_name }}' 11 | paginate_by = 20 12 | 13 | def get_context_data(self, **kwargs): 14 | context = super().get_context_data(**kwargs) 15 | return context 16 | 17 | 18 | urlpatterns.append( 19 | path('{{ namespace }}', {{ classname }}ListView.as_view(), name='{{ name }}-list') 20 | ) 21 | -------------------------------------------------------------------------------- /src/django_clite/template_files/views/update.tpl: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from django.views.generic import UpdateView 3 | from ..urls import urlpatterns 4 | from ..models import {{ classname }} 5 | from ..forms import {{ classname }}Form 6 | 7 | 8 | class {{ classname }}UpdateView(UpdateView): 9 | model = {{ classname }} 10 | form_class = {{ classname }}Form 11 | context_object_name = '{{ name }}' 12 | template_name = '{{ template_name }}' 13 | 14 | def get_context_data(self, **kwargs): 15 | context = super().get_context_data(**kwargs) 16 | return context 17 | 18 | 19 | urlpatterns.append( 20 | path('{{ namespace }}//edit', {{ classname }}UpdateView.as_view(), name='{{ name }}-update') 21 | ) 22 | -------------------------------------------------------------------------------- /src/django_clite/template_files/views/view.tpl: -------------------------------------------------------------------------------- 1 | import datetime 2 | from django.shortcuts import render 3 | # from django.views.decorators.cache import cache_page 4 | # from django.shortcuts import HttpResponse 5 | 6 | 7 | # @cache_page(60 * 1) # cache for 1 minute 8 | def {{ name }}(request): 9 | template = '{{ name }}.html' 10 | context = { 11 | 'date': datetime.datetime.now, 12 | } 13 | 14 | """ 15 | Alternatively, your view can return HTML directly like so: 16 | html = '

{{ classname }}View

It is now %s.' % date 17 | return HttpResponse(html) 18 | """ 19 | return render(request, template, context) 20 | -------------------------------------------------------------------------------- /src/django_clite/template_files/viewsets/test.tpl: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from rest_framework import status 3 | from rest_framework.test import APIClient, APITestCase 4 | 5 | 6 | class {{ classname }}TestCase(APITestCase): 7 | # This client can be used to connect to the API 8 | client = APIClient() 9 | 10 | fixtures = [] 11 | 12 | def setUp(self): 13 | # API endpoint 14 | self.namespace = '/v1/{{ namespace }}' 15 | self.user = get_user_model().objects.get(id=1) 16 | 17 | def test_not_allowed_to_create_record_when_unauthenticated(self): 18 | res = self.client.post(self.namespace, data={}) 19 | self.assertEqual(res.status_code, status.HTTP_403_FORBIDDEN) 20 | -------------------------------------------------------------------------------- /src/django_clite/template_files/viewsets/viewset.tpl: -------------------------------------------------------------------------------- 1 | from rest_framework import viewsets 2 | from rest_framework import permissions 3 | from ..router import router 4 | from ..models import {{ classname }} 5 | from ..serializers import {{ classname }}Serializer 6 | 7 | 8 | class {{ classname }}ViewSet(viewsets.{% if read_only %}ReadOnlyModelViewSet{% else %}ModelViewSet{% endif %}): 9 | queryset = {{ classname }}.objects.all() 10 | serializer_class = {{ classname }}Serializer 11 | permission_classes = [permissions.IsAuthenticatedOrReadOnly] 12 | 13 | 14 | router.register('{{namespace}}', {{ classname }}ViewSet) 15 | -------------------------------------------------------------------------------- /src/django_clite/utils.py: -------------------------------------------------------------------------------- 1 | import re 2 | import inflection 3 | 4 | 5 | def inflect(noun, force_singular=False, force_plural=False): 6 | if force_plural == force_singular: 7 | return noun 8 | elif force_singular: 9 | return inflection.singularize(noun) 10 | elif force_plural: 11 | return inflection.pluralize(noun) 12 | 13 | return noun 14 | 15 | 16 | def sanitized_string(text): 17 | r = inflection.transliterate(text.strip()) 18 | r = re.sub(r"[:.,;\\/?!&$#@)(+=\'\"\-|\s]+", "_", r) 19 | r = r.strip("_") # remove leading or trailing underscores 20 | return inflection.underscore(r).lower() 21 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # tests 2 | import unittest 3 | 4 | from pathlib import Path 5 | 6 | from geny.core.templates.template import TemplateParser 7 | from django_clite import template_files 8 | 9 | temp_dir = Path(__file__).resolve().parent / "tmp" 10 | 11 | parser = TemplateParser( 12 | templates_dir=[Path(template_files.__file__).resolve().parent], 13 | context={}, 14 | ) 15 | 16 | if __name__ == "__main__": 17 | unittest.main() 18 | -------------------------------------------------------------------------------- /tests/commands/__init__.py: -------------------------------------------------------------------------------- 1 | # tests:commands 2 | from tests import parser # noqa: F401 3 | -------------------------------------------------------------------------------- /tests/commands/generators/__init__.py: -------------------------------------------------------------------------------- 1 | # tests:commands:generators 2 | -------------------------------------------------------------------------------- /tests/commands/generators/test_generators.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pathlib 3 | import unittest 4 | 5 | from click.testing import CliRunner 6 | from django.core.management import call_command 7 | from django.core.management.commands import startapp, startproject 8 | 9 | from geny.core.filesystem.filesystem import working_directory 10 | from django_clite.commands.generate.main import generate 11 | from django_clite.commands.destroy.main import destroy 12 | 13 | from tests import temp_dir 14 | 15 | 16 | runner = CliRunner() 17 | 18 | app_name = "blogger" 19 | 20 | 21 | class GeneratorTestCase(unittest.TestCase): 22 | def setUp(self): 23 | # Generates a base app wherein all generators should create their modules 24 | with working_directory(temp_dir): 25 | call_command(startapp.Command(), app_name, verbosity=0) 26 | 27 | def tearDown(self): 28 | # Removes files created in setUp() 29 | for root, dirs, files in os.walk(temp_dir, topdown=False): 30 | root_path = pathlib.Path(root) 31 | for name in files: 32 | if name == ".keep": 33 | continue 34 | (root_path / name).unlink() 35 | for name in dirs: 36 | (root_path / name).rmdir() 37 | 38 | def test_require_app_scope_to_run_generators(self): 39 | with working_directory(temp_dir): 40 | commands = [ 41 | "admin", 42 | "fixture", 43 | "form", 44 | "manager", 45 | "model", 46 | "scaffold", 47 | "serializer", 48 | "signal", 49 | "tag", 50 | "template", 51 | "validator", 52 | "view", 53 | "viewset", 54 | ] 55 | 56 | for command in commands: 57 | with self.subTest(cmd="generate"): 58 | res = runner.invoke(generate, [command, "article"]) # noqa 59 | self.assertNotEqual(0, res.exit_code) 60 | self.assertIn("app was not detected", res.output) 61 | 62 | with self.subTest(cmd="destroy"): 63 | res = runner.invoke(destroy, [command, "article"]) # noqa 64 | self.assertNotEqual(0, res.exit_code) 65 | self.assertIn("app was not detected", res.output) 66 | 67 | def test_require_project_scope_to_generate_dockerfile(self): 68 | cmd = "dockerfile" 69 | 70 | # Failure 71 | 72 | with self.subTest(name="random directory"): 73 | with working_directory(temp_dir): 74 | res = runner.invoke(generate, [cmd]) # noqa 75 | self.assertNotEqual(0, res.exit_code) 76 | self.assertIn("project was not detected", res.output) 77 | 78 | with self.subTest(name="app directory"): 79 | with working_directory(temp_dir / app_name): 80 | res = runner.invoke(generate, [cmd]) # noqa 81 | self.assertNotEqual(0, res.exit_code) 82 | self.assertIn("project was not detected", res.output) 83 | 84 | # Success 85 | 86 | with runner.isolated_filesystem(): 87 | project = "project" 88 | call_command(startproject.Command(), project, verbosity=0) 89 | 90 | with working_directory(project): 91 | with self.subTest(name="no flags"): 92 | res = runner.invoke(generate, [cmd]) # noqa 93 | self.assertEqual(0, res.exit_code) 94 | self.assertIn("Dockerfile", os.listdir()) 95 | 96 | # Destroy files 97 | d_res = runner.invoke(destroy, [cmd]) # noqa 98 | self.assertEqual(0, d_res.exit_code) 99 | self.assertNotIn("Dockerfile", os.listdir()) 100 | 101 | with self.subTest(name="with flags"): 102 | res = runner.invoke(generate, [cmd, "--compose"]) # noqa 103 | self.assertEqual(0, res.exit_code) 104 | self.assertIn("Dockerfile", os.listdir()) 105 | self.assertIn("docker-compose.yaml", os.listdir()) 106 | 107 | # Destroy file 108 | d_res = runner.invoke(destroy, [cmd, "--compose"]) # noqa 109 | 110 | self.assertEqual(0, d_res.exit_code) 111 | self.assertNotIn("Dockerfile", os.listdir()) 112 | self.assertNotIn("docker-compose.yaml", os.listdir()) 113 | 114 | def test_generate_app_modules(self): 115 | generators = { 116 | "admin": "admin", 117 | "form": "forms", 118 | "model": "models", 119 | "serializer": "serializers", 120 | "viewset": "viewsets", 121 | "view": "views", 122 | "tag": "templatetags", 123 | } 124 | 125 | import_pattern = "from .article import [Aa]rticle([a-zA-Z]+)?$" 126 | 127 | for scope, package in generators.items(): 128 | with self.subTest(scope=scope): 129 | with working_directory(temp_dir / app_name): 130 | g_res = runner.invoke(generate, [scope, "article"]) # noqa 131 | 132 | self.assertEqual(0, g_res.exit_code) 133 | self.assertIn(package, os.listdir()) 134 | self.assertIn("article.py", os.listdir(package)) 135 | 136 | # Inspect imports 137 | with open(pathlib.Path(package) / "__init__.py", "r") as f: 138 | self.assertRegex(f.read(), import_pattern) 139 | 140 | # Destroy file 141 | d_res = runner.invoke(destroy, [scope, "article"]) # noqa 142 | 143 | self.assertEqual(0, d_res.exit_code) 144 | self.assertIn(package, os.listdir()) 145 | self.assertNotIn("article.py", os.listdir(package)) 146 | 147 | # Inspect imports 148 | with open(pathlib.Path(package) / "__init__.py", "r") as f: 149 | self.assertEqual(f.read(), "") 150 | 151 | def test_generate_model_scoped_app_modules(self): 152 | generators = { 153 | "manager": "managers", 154 | "signal": "signals", 155 | "validator": "validators", 156 | } 157 | 158 | for scope, package in generators.items(): 159 | with self.subTest(scope=scope): 160 | with working_directory(temp_dir / app_name): 161 | g_res = runner.invoke(generate, [scope, "article"]) # noqa 162 | 163 | self.assertEqual(0, g_res.exit_code) 164 | self.assertIn(package, os.listdir("models")) 165 | self.assertIn("article.py", os.listdir(f"models/{package}")) 166 | 167 | # Inspect imports 168 | with open(pathlib.Path(f"models/{package}") / "__init__.py", "r") as f: 169 | self.assertRegex(f.read(), 'from .article import [Aa]rticle(_?[a-zA-Z]+)?$') 170 | 171 | # Destroy file 172 | d_res = runner.invoke(destroy, [scope, "article"]) # noqa 173 | 174 | self.assertEqual(0, d_res.exit_code) 175 | self.assertIn(package, os.listdir("models")) 176 | self.assertNotIn("article.py", os.listdir(f"models/{package}")) 177 | 178 | # Inspect imports 179 | with open(pathlib.Path(f"models/{package}") / "__init__.py", "r") as f: 180 | self.assertEqual(f.read(), "") 181 | 182 | def test_generate_tests(self): 183 | generators = { 184 | "model": "models", 185 | "viewset": "viewsets", 186 | } 187 | 188 | for scope, package in generators.items(): 189 | with self.subTest(scope=scope): 190 | with working_directory(temp_dir / app_name): 191 | g_res = runner.invoke(generate, ["test", "article", "--scope", scope]) # noqa 192 | 193 | self.assertEqual(0, g_res.exit_code) 194 | self.assertIn(package, os.listdir("tests")) 195 | self.assertIn("article_test.py", os.listdir(f"tests/{package}")) 196 | 197 | # Inspect imports 198 | with open(pathlib.Path(f"tests/{package}") / "__init__.py", "r") as f: 199 | self.assertRegex(f.read(), 'from .article_test import ArticleTestCase$') 200 | 201 | # Destroy file 202 | d_res = runner.invoke(destroy, ["test", "article", "--scope", scope]) # noqa 203 | 204 | self.assertEqual(0, d_res.exit_code) 205 | self.assertIn(package, os.listdir("tests")) 206 | self.assertNotIn("article_test.py", os.listdir(f"tests/{package}")) 207 | 208 | # Inspect imports 209 | with open(pathlib.Path(f"tests/{package}") / "__init__.py", "r") as f: 210 | self.assertEqual(f.read(), "") 211 | 212 | def test_admin_inline(self): 213 | cmd = "admin-inline" 214 | 215 | with working_directory(temp_dir / app_name): 216 | g_res = runner.invoke(generate, [cmd, "article"]) # noqa 217 | 218 | self.assertEqual(0, g_res.exit_code) 219 | self.assertIn("admin", os.listdir()) 220 | self.assertIn("inlines", os.listdir("admin")) 221 | self.assertIn("article.py", os.listdir("admin/inlines")) 222 | 223 | # Inspect imports 224 | with open(pathlib.Path("admin/inlines") / "__init__.py", "r") as f: 225 | self.assertRegex(f.read(), 'from .article import ArticleInline$') 226 | 227 | # Destroy file 228 | d_res = runner.invoke(destroy, [cmd, "article"]) # noqa 229 | 230 | self.assertEqual(0, d_res.exit_code) 231 | self.assertIn("admin", os.listdir()) 232 | self.assertIn("inlines", os.listdir("admin")) 233 | self.assertNotIn("article.py", os.listdir("admin/inlines")) 234 | 235 | # Inspect imports 236 | with open(pathlib.Path("admin/inlines") / "__init__.py", "r") as f: 237 | self.assertEqual(f.read(), "") 238 | 239 | def test_fixture(self): 240 | cmd = "fixture" 241 | package = "fixtures" 242 | 243 | with working_directory(temp_dir / app_name): 244 | g_res = runner.invoke(generate, [cmd, "article"]) # noqa 245 | 246 | self.assertEqual(0, g_res.exit_code) 247 | self.assertIn(package, os.listdir()) 248 | self.assertIn("article.json", os.listdir(package)) 249 | 250 | # Destroy file 251 | d_res = runner.invoke(destroy, [cmd, "article"]) # noqa 252 | 253 | self.assertEqual(0, d_res.exit_code) 254 | self.assertIn(package, os.listdir()) 255 | self.assertNotIn("article.json", os.listdir(package)) 256 | 257 | def test_template(self): 258 | cmd = "template" 259 | dir_ = "templates" 260 | 261 | with working_directory(temp_dir / app_name): 262 | g_res = runner.invoke(generate, [cmd, "article", "--full"]) # noqa 263 | 264 | self.assertEqual(0, g_res.exit_code) 265 | self.assertIn(dir_, os.listdir()) 266 | self.assertEqual( 267 | [ 268 | "article_create.html", 269 | "article_detail.html", 270 | "article_list.html", 271 | "article_update.html", 272 | ], 273 | sorted(os.listdir(dir_)), 274 | ) 275 | 276 | # Destroy file 277 | d_res = runner.invoke(destroy, [cmd, "article", "--full"]) # noqa 278 | 279 | self.assertEqual(0, d_res.exit_code) 280 | self.assertIn(dir_, os.listdir()) 281 | self.assertNotEqual( 282 | [ 283 | "article_create.html", 284 | "article_detail.html", 285 | "article_list.html", 286 | "article_update.html", 287 | ], 288 | sorted(os.listdir(dir_)), 289 | ) 290 | -------------------------------------------------------------------------------- /tests/commands/new/__init__.py: -------------------------------------------------------------------------------- 1 | # tests:commands:new 2 | -------------------------------------------------------------------------------- /tests/commands/new/test_new.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | from pathlib import Path 4 | from click.testing import CliRunner 5 | from geny.core.filesystem.files import File 6 | from geny.core.filesystem.directories import Directory 7 | from django_clite.commands.new.main import new 8 | from tests import parser as _ # noqa: F401 9 | 10 | 11 | runner = CliRunner() 12 | 13 | 14 | class CreatorTestCase(unittest.TestCase): 15 | def test_new_project(self): 16 | with runner.isolated_filesystem(): 17 | command = "project" 18 | proj_name = "store" 19 | res = runner.invoke(new, [command, proj_name]) # noqa 20 | 21 | self.assertEqual(0, res.exit_code) 22 | self.assertEqual(b"", res.stdout_bytes) 23 | 24 | proj_dir = Path(proj_name) 25 | self.assertTrue(proj_dir.exists()) 26 | self.assertTrue(proj_dir.is_dir()) 27 | 28 | with open(Path(proj_dir) / "README.md") as f: 29 | line = f.readline() 30 | self.assertEqual("# store\n", line) 31 | 32 | def test_new_app(self): 33 | with runner.isolated_filesystem(): 34 | cmd = "apps" 35 | app_name = "blogger" 36 | 37 | res = runner.invoke(new, [cmd, app_name]) # noqa 38 | self.assertEqual(0, res.exit_code) 39 | self.assertEqual(b"", res.stdout_bytes) 40 | 41 | app_dir = Path(app_name) 42 | self.assertTrue(app_dir.exists()) 43 | self.assertTrue(app_dir.is_dir()) 44 | 45 | self.assertEqual( 46 | [ 47 | "__init__.py", 48 | "admin", 49 | "apps.py", 50 | "fixtures", 51 | "forms", 52 | "middleware", 53 | "migrations", 54 | "models", 55 | "router", 56 | "serializers", 57 | "tasks", 58 | "templates", 59 | "templatetags", 60 | "tests", 61 | "urls.py", 62 | "views", 63 | "viewsets", 64 | ], 65 | sorted(os.listdir(app_name)), 66 | ) 67 | -------------------------------------------------------------------------------- /tests/core/__init__.py: -------------------------------------------------------------------------------- 1 | # tests:core 2 | -------------------------------------------------------------------------------- /tests/core/field_parser/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oleoneto/django-clite/c47ff018a3ff4fcc202029805fb58bc4f999fda8/tests/core/field_parser/__init__.py -------------------------------------------------------------------------------- /tests/core/field_parser/test_field_parser.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from django_clite.core.field_parser.factory import AttributeFactory 3 | 4 | 5 | class CallbackTestCase(unittest.TestCase): 6 | def test_field_parser(self): 7 | fields = AttributeFactory().parsed_fields( 8 | [ 9 | "name:char", 10 | "title:char", 11 | "user:fk", 12 | "desc:char", 13 | "rating:int", 14 | "owner:fk", 15 | "total:int", 16 | ] 17 | ) 18 | 19 | field_names = sorted(fields.keys()) 20 | self.assertEqual( 21 | ["desc", "name", "owner", "rating", "title", "total", "user"], field_names 22 | ) 23 | 24 | field_values = list(map(lambda x: x.kind, fields.values())) 25 | self.assertEqual( 26 | [ 27 | "CharField", 28 | "CharField", 29 | "ForeignKey", 30 | "CharField", 31 | "IntegerField", 32 | "ForeignKey", 33 | "IntegerField", 34 | ], 35 | field_values, 36 | ) 37 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from django_clite.utils import inflect, sanitized_string 3 | 4 | class UtilsTestCase(unittest.TestCase): 5 | def test_inflection(self): 6 | singulars = { 7 | "woman": "women", 8 | "man": "men", 9 | "ox": "oxen", 10 | "car": "cars", 11 | "child": "children", 12 | } 13 | for word, plural in singulars.items(): 14 | self.assertEqual(inflect(word, force_plural=True), plural) 15 | 16 | plurals = { 17 | "women": "woman", 18 | "men": "man", 19 | "oxen": "ox", 20 | "cars": "car", 21 | "children": "child", 22 | } 23 | for word, singular in plurals.items(): 24 | self.assertEqual(inflect(word, force_singular=True), singular) 25 | 26 | def test_sanitized_string(self): 27 | string = " Once-Upon a Time " 28 | self.assertEqual(sanitized_string(string), "once_upon_a_time") 29 | 30 | string = "/my.example/site-1_/" 31 | self.assertEqual(sanitized_string(string), "my_example_site_1") 32 | -------------------------------------------------------------------------------- /tests/tmp/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oleoneto/django-clite/c47ff018a3ff4fcc202029805fb58bc4f999fda8/tests/tmp/.keep --------------------------------------------------------------------------------