├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── README.md ├── generatedfields ├── __init__.py ├── asgi.py ├── settings.py ├── urls.py └── wsgi.py ├── manage.py ├── pyproject.toml ├── requirements.txt └── samples ├── __init__.py ├── admin.py ├── apps.py ├── migrations ├── 0001_initial.py ├── 0002_square.py ├── 0003_circle.py ├── 0004_righttriangle.py ├── 0005_item.py ├── 0006_order.py ├── 0007_event.py ├── 0008_package.py ├── 0009_user.py └── __init__.py ├── models.py ├── tests.py └── views.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | default_language_version: 2 | python: python3.12 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: "v4.5.0" 6 | hooks: 7 | - id: check-added-large-files 8 | args: ["--maxkb=1024"] 9 | - id: check-case-conflict 10 | - id: check-docstring-first 11 | - id: check-merge-conflict 12 | - id: check-toml 13 | - id: check-yaml 14 | args: ["--allow-multiple-documents"] 15 | - id: debug-statements 16 | - id: detect-private-key 17 | - id: end-of-file-fixer 18 | - id: fix-byte-order-marker 19 | - id: fix-encoding-pragma 20 | args: ["--remove"] 21 | - id: mixed-line-ending 22 | - id: trailing-whitespace 23 | - repo: https://github.com/charliermarsh/ruff-pre-commit 24 | rev: "v0.1.6" 25 | hooks: 26 | - id: ruff 27 | args: [--fix, --exit-non-zero-on-fix] 28 | - id: ruff-format 29 | - repo: https://github.com/tox-dev/pyproject-fmt 30 | rev: "1.5.1" 31 | hooks: 32 | - id: pyproject-fmt 33 | - repo: https://github.com/adamchainz/blacken-docs 34 | rev: "1.16.0" 35 | hooks: 36 | - id: blacken-docs 37 | additional_dependencies: 38 | - black==23.10.0 39 | - repo: https://github.com/pre-commit/mirrors-prettier 40 | rev: "v3.1.0" 41 | hooks: 42 | - id: prettier 43 | - repo: https://github.com/pre-commit/mirrors-mypy 44 | rev: "v1.7.0" 45 | hooks: 46 | - id: mypy 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023-2024 Paolo Melchiorre 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django Generated Fields examples 2 | 3 | Code related to the article [Database generated columns ⁽¹⁾: Django & SQLite](https://www.paulox.net/2023/11/07/database-generated-columns-part-1-django-and-sqlite/), to be used to experiment with the new `GeneratedField` added in Django 5.0. 4 | 5 | [![© 2023 Paolo Melchiorre “View of clouds over the Labrador Peninsula (Canada) taken from a commercial flight.”](https://www.paulox.net/images/derivatives/wide/1050w/1000014469-01.webp "© 2023 Paolo Melchiorre CC BY-NC-SA “© 2023 Paolo Melchiorre “View of clouds over the Labrador Peninsula (Canada) taken from a commercial flight.”")](https://www.paulox.net/2023/11/07/database-generated-columns-part-1-django-and-sqlite/) 6 | 7 | > View of clouds over the Labrador Peninsula (Canada) taken from a commercial flight. 8 | 9 | ## 💻 Set Up 10 | 11 | ### ⚗️ Virtual environment 12 | 13 | Creating and activating the virtual environment: 14 | 15 | ```console 16 | $ python3 -m venv .venv 17 | $ source .venv/bin/activate 18 | ``` 19 | 20 | ### 🦄 Django 21 | 22 | Installing the latest version of Django (tested with Django 5.0-5.1): 23 | 24 | ```console 25 | $ python -m pip install -r requirements.txt 26 | ``` 27 | 28 | ## 🐚 Shell 29 | 30 | Start the shell with the command: 31 | 32 | ```console 33 | $ python -m manage shell 34 | ``` 35 | 36 | ## 🔬 Tests 37 | 38 | Running the defined tests: 39 | 40 | ```console 41 | $ python -m manage test 42 | ``` 43 | 44 | ## ⚖️ License 45 | 46 | The **Django Generated Fields examples** project is licensed under the [MIT License](https://github.com/pauloxnet/generatedfields/blob/main/LICENSE). 47 | 48 | ## 👥 Authors 49 | 50 | ### 👤 Paolo Melchiorre 51 | 52 | - 🌍 Blog: [www.paulox.net](https://www.paulox.net) 53 | - 🐙 Github: [@pauloxnet@github.com](https://github.com/pauloxnet) 54 | - 🦣 Mastodon: [@paulox@fosstodon.org](https://fosstodon.org/@paulox) 55 | - 🐦️ Twitter: [@pauloxnet@twitter.com](https://twitter.com/pauloxnet) 56 | -------------------------------------------------------------------------------- /generatedfields/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pauloxnet/generatedfields/a120177707786d4fbd7a0ff5c4385bf205d3fb47/generatedfields/__init__.py -------------------------------------------------------------------------------- /generatedfields/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for generatedfields project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/dev/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "generatedfields.settings") 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /generatedfields/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for generatedfields project. 3 | 4 | Generated by 'django-admin startproject' using Django 5.0rc1. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/dev/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/dev/ref/settings/ 11 | """ 12 | 13 | from pathlib import Path 14 | 15 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 16 | BASE_DIR = Path(__file__).resolve().parent.parent 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/dev/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = "django-insecure-#s!x(p!d15r(%ao!&56)aks5+d5(&@e5+9$i-x6$47u463-%1-" 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS: list = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | "django.contrib.admin", 35 | "django.contrib.auth", 36 | "django.contrib.contenttypes", 37 | "django.contrib.sessions", 38 | "django.contrib.messages", 39 | "django.contrib.staticfiles", 40 | "samples", 41 | ] 42 | 43 | MIDDLEWARE = [ 44 | "django.middleware.security.SecurityMiddleware", 45 | "django.contrib.sessions.middleware.SessionMiddleware", 46 | "django.middleware.common.CommonMiddleware", 47 | "django.middleware.csrf.CsrfViewMiddleware", 48 | "django.contrib.auth.middleware.AuthenticationMiddleware", 49 | "django.contrib.messages.middleware.MessageMiddleware", 50 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 51 | ] 52 | 53 | ROOT_URLCONF = "generatedfields.urls" 54 | 55 | TEMPLATES = [ 56 | { 57 | "BACKEND": "django.template.backends.django.DjangoTemplates", 58 | "DIRS": [], 59 | "APP_DIRS": True, 60 | "OPTIONS": { 61 | "context_processors": [ 62 | "django.template.context_processors.debug", 63 | "django.template.context_processors.request", 64 | "django.contrib.auth.context_processors.auth", 65 | "django.contrib.messages.context_processors.messages", 66 | ], 67 | }, 68 | }, 69 | ] 70 | 71 | WSGI_APPLICATION = "generatedfields.wsgi.application" 72 | 73 | 74 | # Database 75 | # https://docs.djangoproject.com/en/dev/ref/settings/#databases 76 | 77 | DATABASES = { 78 | "default": { 79 | "ENGINE": "django.db.backends.sqlite3", 80 | "NAME": BASE_DIR / "db.sqlite3", 81 | } 82 | } 83 | 84 | 85 | # Password validation 86 | # https://docs.djangoproject.com/en/dev/ref/settings/#auth-password-validators 87 | 88 | AUTH_PASSWORD_VALIDATORS = [ 89 | { 90 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", # noqa: E501 91 | }, 92 | { 93 | "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", 94 | }, 95 | { 96 | "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", 97 | }, 98 | { 99 | "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", 100 | }, 101 | ] 102 | 103 | 104 | # Internationalization 105 | # https://docs.djangoproject.com/en/dev/topics/i18n/ 106 | 107 | LANGUAGE_CODE = "en-us" 108 | 109 | TIME_ZONE = "UTC" 110 | 111 | USE_I18N = True 112 | 113 | USE_TZ = True 114 | 115 | 116 | # Static files (CSS, JavaScript, Images) 117 | # https://docs.djangoproject.com/en/dev/howto/static-files/ 118 | 119 | STATIC_URL = "static/" 120 | 121 | # Default primary key field type 122 | # https://docs.djangoproject.com/en/dev/ref/settings/#default-auto-field 123 | 124 | DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" 125 | -------------------------------------------------------------------------------- /generatedfields/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | URL configuration for generatedfields project. 3 | 4 | The `urlpatterns` list routes URLs to views. For more information please see: 5 | https://docs.djangoproject.com/en/dev/topics/http/urls/ 6 | Examples: 7 | Function views 8 | 1. Add an import: from my_app import views 9 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 10 | Class-based views 11 | 1. Add an import: from other_app.views import Home 12 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 13 | Including another URLconf 14 | 1. Import the include() function: from django.urls import include, path 15 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 16 | """ 17 | from django.contrib import admin 18 | from django.urls import path 19 | 20 | urlpatterns = [ 21 | path("admin/", admin.site.urls), 22 | ] 23 | -------------------------------------------------------------------------------- /generatedfields/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for generatedfields project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/dev/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "generatedfields.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "generatedfields.settings") 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == "__main__": 22 | main() 23 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.ruff] 2 | extend-select = [ 3 | "ASYNC", # flake8-async 4 | "B", # flake8-bugbear 5 | "C4", # flake8-comprehensions 6 | "C90", # McCabe cyclomatic complexity 7 | "DJ", # flake8-django 8 | "E", # pycodestyle errors 9 | "F", # Pyflakes 10 | "FBT", # flake8-boolean-trap 11 | "I", # isort 12 | "INT", # flake8-gettext 13 | "PGH", # pygrep-hooks 14 | "PIE", # flake8-pie 15 | "RUF100", # Unused noqa directive 16 | "SIM", # flake8-simplify 17 | "SLOT", # flake8-slots 18 | "UP", # pyupgrade 19 | "W", # pycodestyle warnings 20 | ] 21 | fix = true 22 | show-fixes = true 23 | target-version = "py312" 24 | 25 | [tool.ruff.isort] 26 | combine-as-imports = true 27 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | django~=5.1.0 2 | -------------------------------------------------------------------------------- /samples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pauloxnet/generatedfields/a120177707786d4fbd7a0ff5c4385bf205d3fb47/samples/__init__.py -------------------------------------------------------------------------------- /samples/admin.py: -------------------------------------------------------------------------------- 1 | # Register your models here. 2 | -------------------------------------------------------------------------------- /samples/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class SamplesConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "samples" 7 | -------------------------------------------------------------------------------- /samples/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | import django.db.models.expressions 2 | from django.db import migrations, models 3 | 4 | 5 | class Migration(migrations.Migration): 6 | initial = True 7 | 8 | dependencies: list = [] 9 | 10 | operations = [ 11 | migrations.CreateModel( 12 | name="Rectangle", 13 | fields=[ 14 | ( 15 | "id", 16 | models.BigAutoField( 17 | auto_created=True, 18 | primary_key=True, 19 | serialize=False, 20 | verbose_name="ID", 21 | ), 22 | ), 23 | ("base", models.FloatField()), 24 | ("height", models.FloatField()), 25 | ( 26 | "area", 27 | models.GeneratedField( 28 | db_persist=True, 29 | expression=django.db.models.expressions.CombinedExpression( 30 | models.F("base"), "*", models.F("height") 31 | ), 32 | output_field=models.FloatField(), 33 | ), 34 | ), 35 | ], 36 | ), 37 | ] 38 | -------------------------------------------------------------------------------- /samples/migrations/0002_square.py: -------------------------------------------------------------------------------- 1 | import django.db.models.functions.math 2 | from django.db import migrations, models 3 | 4 | 5 | class Migration(migrations.Migration): 6 | dependencies = [ 7 | ("samples", "0001_initial"), 8 | ] 9 | 10 | operations = [ 11 | migrations.CreateModel( 12 | name="Square", 13 | fields=[ 14 | ( 15 | "id", 16 | models.BigAutoField( 17 | auto_created=True, 18 | primary_key=True, 19 | serialize=False, 20 | verbose_name="ID", 21 | ), 22 | ), 23 | ("side", models.FloatField()), 24 | ( 25 | "area", 26 | models.GeneratedField( 27 | db_persist=True, 28 | expression=django.db.models.functions.math.Power("side", 2), 29 | output_field=models.FloatField(), 30 | ), 31 | ), 32 | ], 33 | ), 34 | ] 35 | -------------------------------------------------------------------------------- /samples/migrations/0003_circle.py: -------------------------------------------------------------------------------- 1 | import django.db.models.expressions 2 | import django.db.models.functions.math 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("samples", "0002_square"), 9 | ] 10 | 11 | operations = [ 12 | migrations.CreateModel( 13 | name="Circle", 14 | fields=[ 15 | ( 16 | "id", 17 | models.BigAutoField( 18 | auto_created=True, 19 | primary_key=True, 20 | serialize=False, 21 | verbose_name="ID", 22 | ), 23 | ), 24 | ("radius", models.FloatField()), 25 | ( 26 | "area", 27 | models.GeneratedField( 28 | db_persist=True, 29 | expression=django.db.models.functions.math.Round( 30 | django.db.models.expressions.CombinedExpression( 31 | django.db.models.functions.math.Power("radius", 2), 32 | "*", 33 | django.db.models.functions.math.Pi(), 34 | ), 35 | precision=2, 36 | ), 37 | output_field=models.FloatField(), 38 | ), 39 | ), 40 | ], 41 | ), 42 | ] 43 | -------------------------------------------------------------------------------- /samples/migrations/0004_righttriangle.py: -------------------------------------------------------------------------------- 1 | import django.db.models.expressions 2 | import django.db.models.functions.math 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("samples", "0003_circle"), 9 | ] 10 | 11 | operations = [ 12 | migrations.CreateModel( 13 | name="RightTriangle", 14 | fields=[ 15 | ( 16 | "id", 17 | models.BigAutoField( 18 | auto_created=True, 19 | primary_key=True, 20 | serialize=False, 21 | verbose_name="ID", 22 | ), 23 | ), 24 | ("hypotenuse", models.FloatField()), 25 | ("angle", models.FloatField()), 26 | ( 27 | "area", 28 | models.GeneratedField( 29 | db_persist=True, 30 | expression=django.db.models.functions.math.Round( 31 | django.db.models.expressions.CombinedExpression( 32 | django.db.models.expressions.CombinedExpression( 33 | django.db.models.expressions.CombinedExpression( 34 | django.db.models.functions.math.Power( 35 | "hypotenuse", 2 36 | ), 37 | "*", 38 | django.db.models.functions.math.Sin( 39 | django.db.models.functions.math.Radians( 40 | "angle" 41 | ) 42 | ), 43 | ), 44 | "*", 45 | django.db.models.functions.math.Cos( 46 | django.db.models.functions.math.Radians("angle") 47 | ), 48 | ), 49 | "/", 50 | models.Value(2), 51 | ), 52 | precision=2, 53 | ), 54 | output_field=models.FloatField(), 55 | ), 56 | ), 57 | ], 58 | ), 59 | ] 60 | -------------------------------------------------------------------------------- /samples/migrations/0005_item.py: -------------------------------------------------------------------------------- 1 | import django.db.models.expressions 2 | from django.db import migrations, models 3 | 4 | 5 | class Migration(migrations.Migration): 6 | dependencies = [ 7 | ("samples", "0004_righttriangle"), 8 | ] 9 | 10 | operations = [ 11 | migrations.CreateModel( 12 | name="Item", 13 | fields=[ 14 | ( 15 | "id", 16 | models.BigAutoField( 17 | auto_created=True, 18 | primary_key=True, 19 | serialize=False, 20 | verbose_name="ID", 21 | ), 22 | ), 23 | ("price", models.DecimalField(decimal_places=2, max_digits=6)), 24 | ( 25 | "quantity", 26 | models.PositiveSmallIntegerField(db_default=models.Value(1)), 27 | ), 28 | ( 29 | "total_price", 30 | models.GeneratedField( 31 | db_persist=True, 32 | expression=django.db.models.expressions.CombinedExpression( 33 | models.F("price"), "*", models.F("quantity") 34 | ), 35 | output_field=models.DecimalField( 36 | decimal_places=2, max_digits=11 37 | ), 38 | ), 39 | ), 40 | ], 41 | ), 42 | ] 43 | -------------------------------------------------------------------------------- /samples/migrations/0006_order.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations, models 2 | 3 | 4 | class Migration(migrations.Migration): 5 | dependencies = [ 6 | ("samples", "0005_item"), 7 | ] 8 | 9 | operations = [ 10 | migrations.CreateModel( 11 | name="Order", 12 | fields=[ 13 | ( 14 | "id", 15 | models.BigAutoField( 16 | auto_created=True, 17 | primary_key=True, 18 | serialize=False, 19 | verbose_name="ID", 20 | ), 21 | ), 22 | ("creation", models.DateTimeField()), 23 | ("payment", models.DateTimeField(null=True)), 24 | ( 25 | "status", 26 | models.GeneratedField( 27 | db_persist=True, 28 | expression=models.Case( 29 | models.When( 30 | payment__isnull=False, then=models.Value("paid") 31 | ), 32 | default=models.Value("created"), 33 | ), 34 | output_field=models.TextField(), 35 | ), 36 | ), 37 | ], 38 | ), 39 | ] 40 | -------------------------------------------------------------------------------- /samples/migrations/0007_event.py: -------------------------------------------------------------------------------- 1 | import django.db.models.expressions 2 | import django.db.models.functions.datetime 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("samples", "0006_order"), 9 | ] 10 | 11 | operations = [ 12 | migrations.CreateModel( 13 | name="Event", 14 | fields=[ 15 | ( 16 | "id", 17 | models.BigAutoField( 18 | auto_created=True, 19 | primary_key=True, 20 | serialize=False, 21 | verbose_name="ID", 22 | ), 23 | ), 24 | ("start", models.DateTimeField()), 25 | ( 26 | "start_date", 27 | models.GeneratedField( 28 | db_persist=True, 29 | expression=django.db.models.functions.datetime.TruncDate( 30 | "start" 31 | ), 32 | output_field=models.DateField(), 33 | ), 34 | ), 35 | ("end", models.DateTimeField(null=True)), 36 | ( 37 | "end_date", 38 | models.GeneratedField( 39 | db_persist=True, 40 | expression=django.db.models.functions.datetime.TruncDate("end"), 41 | output_field=models.DateField(), 42 | ), 43 | ), 44 | ( 45 | "duration", 46 | models.GeneratedField( 47 | db_persist=True, 48 | expression=django.db.models.expressions.CombinedExpression( 49 | models.F("end"), "-", models.F("start") 50 | ), 51 | output_field=models.DurationField(), 52 | ), 53 | ), 54 | ], 55 | ), 56 | ] 57 | -------------------------------------------------------------------------------- /samples/migrations/0008_package.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations, models 2 | 3 | 4 | class Migration(migrations.Migration): 5 | dependencies = [ 6 | ("samples", "0007_event"), 7 | ] 8 | 9 | operations = [ 10 | migrations.CreateModel( 11 | name="Package", 12 | fields=[ 13 | ( 14 | "id", 15 | models.BigAutoField( 16 | auto_created=True, 17 | primary_key=True, 18 | serialize=False, 19 | verbose_name="ID", 20 | ), 21 | ), 22 | ("slug", models.SlugField()), 23 | ("data", models.JSONField()), 24 | ( 25 | "version", 26 | models.GeneratedField( 27 | db_persist=True, 28 | expression=models.F("data__info__version"), 29 | output_field=models.TextField(), 30 | ), 31 | ), 32 | ], 33 | ), 34 | ] 35 | -------------------------------------------------------------------------------- /samples/migrations/0009_user.py: -------------------------------------------------------------------------------- 1 | import django.db.models.functions.text 2 | from django.db import migrations, models 3 | 4 | 5 | class Migration(migrations.Migration): 6 | dependencies = [ 7 | ("samples", "0008_package"), 8 | ] 9 | 10 | operations = [ 11 | migrations.CreateModel( 12 | name="User", 13 | fields=[ 14 | ( 15 | "id", 16 | models.BigAutoField( 17 | auto_created=True, 18 | primary_key=True, 19 | serialize=False, 20 | verbose_name="ID", 21 | ), 22 | ), 23 | ("first_name", models.CharField(max_length=150)), 24 | ("last_name", models.CharField(max_length=150)), 25 | ( 26 | "full_name", 27 | models.GeneratedField( 28 | db_persist=True, 29 | expression=django.db.models.functions.text.Concat( 30 | "first_name", models.Value(" "), "last_name" 31 | ), 32 | output_field=models.TextField(), 33 | ), 34 | ), 35 | ], 36 | ), 37 | ] 38 | -------------------------------------------------------------------------------- /samples/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pauloxnet/generatedfields/a120177707786d4fbd7a0ff5c4385bf205d3fb47/samples/migrations/__init__.py -------------------------------------------------------------------------------- /samples/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.db.models import Case, F, Value, When 3 | from django.db.models.functions import ( 4 | Concat, 5 | Cos, 6 | Pi, 7 | Power, 8 | Radians, 9 | Round, 10 | Sin, 11 | TruncDate, 12 | ) 13 | 14 | 15 | class Rectangle(models.Model): 16 | base = models.FloatField() 17 | height = models.FloatField() 18 | area = models.GeneratedField( 19 | expression=F("base") * F("height"), 20 | output_field=models.FloatField(), 21 | db_persist=True, 22 | ) 23 | 24 | def __str__(self): 25 | return f"{self.base}×{self.height}={self.area}" 26 | 27 | 28 | class Square(models.Model): 29 | side = models.FloatField() 30 | area = models.GeneratedField( 31 | expression=Power("side", 2), 32 | output_field=models.FloatField(), 33 | db_persist=True, 34 | ) 35 | 36 | def __str__(self): 37 | return f"{self.side}²={self.area}" 38 | 39 | 40 | class Circle(models.Model): 41 | radius = models.FloatField() 42 | area = models.GeneratedField( 43 | expression=Round( 44 | Power("radius", 2) * Pi(), 45 | precision=2, 46 | ), 47 | output_field=models.FloatField(), 48 | db_persist=True, 49 | ) 50 | 51 | def __str__(self): 52 | return f"{self.radius}²×π={self.area}" 53 | 54 | 55 | class RightTriangle(models.Model): 56 | hypotenuse = models.FloatField() 57 | angle = models.FloatField() 58 | area = models.GeneratedField( 59 | expression=Round( 60 | (Power("hypotenuse", 2) * Sin(Radians("angle")) * Cos(Radians("angle"))) 61 | / 2, 62 | precision=2, 63 | ), 64 | output_field=models.FloatField(), 65 | db_persist=True, 66 | ) 67 | 68 | def __str__(self): 69 | return f"{self.hypotenuse}²×sin({self.angle}°)×cos({self.angle}°)÷2={self.area}" 70 | 71 | 72 | class Item(models.Model): 73 | price = models.DecimalField(max_digits=6, decimal_places=2) 74 | quantity = models.PositiveSmallIntegerField(db_default=Value(1)) 75 | total_price = models.GeneratedField( 76 | expression=F("price") * F("quantity"), 77 | output_field=models.DecimalField(max_digits=11, decimal_places=2), 78 | db_persist=True, 79 | ) 80 | 81 | def __str__(self): 82 | return f"{self.price}×{self.quantity}={self.total_price}" 83 | 84 | 85 | class Order(models.Model): 86 | creation = models.DateTimeField() 87 | payment = models.DateTimeField(null=True) 88 | status = models.GeneratedField( 89 | expression=Case( 90 | When( 91 | payment__isnull=False, 92 | then=Value("paid"), 93 | ), 94 | default=Value("created"), 95 | ), 96 | output_field=models.TextField(), 97 | db_persist=True, 98 | ) 99 | 100 | def __str__(self): 101 | return f"[{self.status}] {self.payment or self.creation}" 102 | 103 | 104 | class Event(models.Model): 105 | start = models.DateTimeField() 106 | start_date = models.GeneratedField( 107 | expression=TruncDate("start"), 108 | output_field=models.DateField(), 109 | db_persist=True, 110 | ) 111 | end = models.DateTimeField(null=True) 112 | end_date = models.GeneratedField( 113 | expression=TruncDate("end"), 114 | output_field=models.DateField(), 115 | db_persist=True, 116 | ) 117 | duration = models.GeneratedField( 118 | expression=F("end") - F("start"), 119 | output_field=models.DurationField(), 120 | db_persist=True, 121 | ) 122 | 123 | def __str__(self): 124 | return f"[{self.duration or '∞'}] {self.start_date}…{self.end_date or ''}" 125 | 126 | 127 | class Package(models.Model): 128 | slug = models.SlugField() 129 | data = models.JSONField() 130 | version = models.GeneratedField( 131 | expression=F("data__info__version"), 132 | output_field=models.TextField(), 133 | db_persist=True, 134 | ) 135 | 136 | def __str__(self): 137 | return f"{self.slug} {self.version}" 138 | 139 | 140 | class User(models.Model): 141 | first_name = models.CharField(max_length=150) 142 | last_name = models.CharField(max_length=150) 143 | full_name = models.GeneratedField( 144 | expression=Concat("first_name", Value(" "), "last_name"), 145 | output_field=models.TextField(), 146 | db_persist=True, 147 | ) 148 | 149 | def __str__(self): 150 | return self.full_name 151 | -------------------------------------------------------------------------------- /samples/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from samples.models import ( 4 | Circle, 5 | Event, 6 | Item, 7 | Order, 8 | Package, 9 | Rectangle, 10 | RightTriangle, 11 | Square, 12 | User, 13 | ) 14 | 15 | 16 | class RectangleTestCase(TestCase): 17 | @classmethod 18 | def setUpTestData(cls): 19 | cls.rectangle = Rectangle.objects.create(base=6, height=7) 20 | 21 | def test_str(self): 22 | self.assertEqual(str(self.rectangle), "6×7=42.0") 23 | 24 | 25 | class SquareTestCase(TestCase): 26 | @classmethod 27 | def setUpTestData(cls): 28 | cls.square = Square.objects.create(side=3) 29 | 30 | def test_str(self): 31 | self.assertEqual(str(self.square), "3²=9.0") 32 | 33 | 34 | class CircleTestCase(TestCase): 35 | @classmethod 36 | def setUpTestData(cls): 37 | cls.circle = Circle.objects.create(radius=3.1415) 38 | 39 | def test_str(self): 40 | self.assertEqual(str(self.circle), "3.1415²×π=31.0") 41 | 42 | 43 | class RightTriangleTestCase(TestCase): 44 | @classmethod 45 | def setUpTestData(cls): 46 | cls.righttriangle = RightTriangle.objects.create(hypotenuse=5, angle=45) 47 | 48 | def test_str(self): 49 | self.assertEqual(str(self.righttriangle), "5²×sin(45°)×cos(45°)÷2=6.25") 50 | 51 | 52 | class ItemTestCase(TestCase): 53 | @classmethod 54 | def setUpTestData(cls): 55 | cls.single_item = Item.objects.create(price=9.99) 56 | cls.multiple_item = Item.objects.create(price=4.99, quantity=2) 57 | 58 | def test_str(self): 59 | self.assertEqual(str(self.single_item), "9.99×1=9.99") 60 | self.assertEqual(str(self.multiple_item), "4.99×2=9.98") 61 | 62 | 63 | class OrderTestCase(TestCase): 64 | @classmethod 65 | def setUpTestData(cls): 66 | cls.createdorder = Order.objects.create(creation="2023-01-01 12:00Z") 67 | cls.paidorder = Order.objects.create( 68 | creation="2023-01-02 00:00Z", 69 | payment="2023-01-03 06:30Z", 70 | ) 71 | 72 | def test_str(self): 73 | self.assertEqual(str(self.createdorder), "[created] 2023-01-01 12:00Z") 74 | self.assertEqual(str(self.paidorder), "[paid] 2023-01-03 06:30Z") 75 | 76 | 77 | class EventTestCase(TestCase): 78 | @classmethod 79 | def setUpTestData(cls): 80 | cls.startevent = Event.objects.create(start="2023-1-1 12:00Z") 81 | cls.endevent = Event.objects.create( 82 | start="2023-1-1 11:45Z", end="2023-1-9 00:00Z" 83 | ) 84 | 85 | def test_str(self): 86 | self.assertEqual(str(self.startevent), "[∞] 2023-01-01…") 87 | self.assertEqual(str(self.endevent), "[7 days, 12:15:00] 2023-01-01…2023-01-09") 88 | 89 | 90 | class PackageTestCase(TestCase): 91 | @classmethod 92 | def setUpTestData(cls): 93 | cls.package = Package.objects.create( 94 | slug="django", data={"info": {"version": "4.2.7"}} 95 | ) 96 | 97 | def test_str(self): 98 | self.assertEqual(str(self.package), "django 4.2.7") 99 | 100 | 101 | class UserTestCase(TestCase): 102 | @classmethod 103 | def setUpTestData(cls): 104 | cls.user = User.objects.create(first_name="Jane", last_name="Doe") 105 | 106 | def test_str(self): 107 | self.assertEqual(str(self.user), "Jane Doe") 108 | -------------------------------------------------------------------------------- /samples/views.py: -------------------------------------------------------------------------------- 1 | # Create your views here. 2 | --------------------------------------------------------------------------------