mirror of
https://github.com/django/django.git
synced 2025-11-18 02:56:45 +00:00
Merge branch 'django:main' into patch-1
This commit is contained in:
commit
c9f6cadba5
268 changed files with 5801 additions and 1163 deletions
1
.gitattributes
vendored
1
.gitattributes
vendored
|
|
@ -4,6 +4,5 @@
|
|||
*js text eol=lf
|
||||
tests/staticfiles_tests/apps/test/static/test/*txt text eol=lf
|
||||
tests/staticfiles_tests/project/documents/test/*txt text eol=lf
|
||||
docs/releases/*.txt merge=union
|
||||
# Make GitHub syntax-highlight .html files as Django templates
|
||||
*.html linguist-language=django
|
||||
|
|
|
|||
6
.github/workflows/docs.yml
vendored
6
.github/workflows/docs.yml
vendored
|
|
@ -29,7 +29,7 @@ jobs:
|
|||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.13'
|
||||
python-version: '3.14'
|
||||
cache: 'pip'
|
||||
cache-dependency-path: 'docs/requirements.txt'
|
||||
- run: python -m pip install -r docs/requirements.txt
|
||||
|
|
@ -47,7 +47,7 @@ jobs:
|
|||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.13'
|
||||
python-version: '3.14'
|
||||
- run: python -m pip install blacken-docs
|
||||
- name: Build docs
|
||||
run: |
|
||||
|
|
@ -68,7 +68,7 @@ jobs:
|
|||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.13'
|
||||
python-version: '3.14'
|
||||
- run: python -m pip install sphinx-lint
|
||||
- name: Build docs
|
||||
run: |
|
||||
|
|
|
|||
6
.github/workflows/linters.yml
vendored
6
.github/workflows/linters.yml
vendored
|
|
@ -27,7 +27,7 @@ jobs:
|
|||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.13'
|
||||
python-version: '3.14'
|
||||
- run: python -m pip install flake8
|
||||
- name: flake8
|
||||
# Pinned to v3.0.0.
|
||||
|
|
@ -44,8 +44,8 @@ jobs:
|
|||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.13'
|
||||
- run: python -m pip install "isort<6"
|
||||
python-version: '3.14'
|
||||
- run: python -m pip install isort
|
||||
- name: isort
|
||||
# Pinned to v3.0.0.
|
||||
uses: liskin/gh-problem-matcher-wrap@e7b7beaaafa52524748b31a381160759d68d61fb
|
||||
|
|
|
|||
7
.github/workflows/new_contributor_pr.yml
vendored
7
.github/workflows/new_contributor_pr.yml
vendored
|
|
@ -12,9 +12,14 @@ jobs:
|
|||
name: Hello new contributor
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/first-interaction@v3
|
||||
# Pin to v1: https://github.com/actions/first-interaction/issues/369
|
||||
- uses: actions/first-interaction@v1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
issue-message: |
|
||||
Hello! Thank you for your interest in Django 💪
|
||||
|
||||
Django issues are tracked in [Trac](https://code.djangoproject.com/) and not in this repo.
|
||||
pr-message: |
|
||||
Hello! Thank you for your contribution 💪
|
||||
|
||||
|
|
|
|||
6
.github/workflows/postgis.yml
vendored
6
.github/workflows/postgis.yml
vendored
|
|
@ -20,7 +20,7 @@ jobs:
|
|||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
postgis-version: [latest, "17-3.5-alpine", "17-master"]
|
||||
postgis-version: ["latest", "18-3.6-alpine", "17-master"]
|
||||
name: PostGIS ${{ matrix.postgis-version }}
|
||||
services:
|
||||
postgres:
|
||||
|
|
@ -42,7 +42,7 @@ jobs:
|
|||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.13'
|
||||
python-version: '3.14'
|
||||
cache: 'pip'
|
||||
cache-dependency-path: 'tests/requirements/py3.txt'
|
||||
- name: Update apt repo
|
||||
|
|
@ -59,7 +59,7 @@ jobs:
|
|||
run: |
|
||||
PGPASSWORD=$POSTGRES_PASSWORD psql -U $POSTGRES_USER -d $POSTGRES_DB -h localhost -c "SELECT PostGIS_full_version();"
|
||||
- name: Install and upgrade packaging tools
|
||||
run: python -m pip install --upgrade pip setuptools wheel
|
||||
run: python -m pip install --upgrade pip wheel
|
||||
- run: python -m pip install -r tests/requirements/py3.txt -r tests/requirements/postgres.txt -e .
|
||||
- name: Create PostgreSQL settings file
|
||||
run: mv ./.github/workflows/data/test_postgis.py.tpl ./tests/test_postgis.py
|
||||
|
|
|
|||
2
.github/workflows/python_matrix.yml
vendored
2
.github/workflows/python_matrix.yml
vendored
|
|
@ -46,7 +46,7 @@ jobs:
|
|||
- name: Install libmemcached-dev for pylibmc
|
||||
run: sudo apt-get install libmemcached-dev
|
||||
- name: Install and upgrade packaging tools
|
||||
run: python -m pip install --upgrade pip setuptools wheel
|
||||
run: python -m pip install --upgrade pip wheel
|
||||
- run: python -m pip install -r tests/requirements/py3.txt -e .
|
||||
- name: Run tests
|
||||
run: python -Wall tests/runtests.py -v2
|
||||
|
|
|
|||
25
.github/workflows/schedule_tests.yml
vendored
25
.github/workflows/schedule_tests.yml
vendored
|
|
@ -18,7 +18,8 @@ jobs:
|
|||
python-version:
|
||||
- '3.12'
|
||||
- '3.13'
|
||||
- '3.14-dev'
|
||||
- '3.14'
|
||||
- '3.15-dev'
|
||||
name: Windows, SQLite, Python ${{ matrix.python-version }}
|
||||
continue-on-error: true
|
||||
steps:
|
||||
|
|
@ -31,7 +32,7 @@ jobs:
|
|||
cache: 'pip'
|
||||
cache-dependency-path: 'tests/requirements/py3.txt'
|
||||
- name: Install and upgrade packaging tools
|
||||
run: python -m pip install --upgrade pip setuptools wheel
|
||||
run: python -m pip install --upgrade pip wheel
|
||||
- run: python -m pip install -r tests/requirements/py3.txt -e .
|
||||
- name: Run tests
|
||||
run: python -Wall tests/runtests.py -v2
|
||||
|
|
@ -45,12 +46,12 @@ jobs:
|
|||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.13'
|
||||
python-version: '3.14'
|
||||
cache: 'pip'
|
||||
- name: Install libmemcached-dev for pylibmc
|
||||
run: sudo apt-get install libmemcached-dev
|
||||
- name: Install and upgrade packaging tools
|
||||
run: python -m pip install --upgrade pip setuptools wheel
|
||||
run: python -m pip install --upgrade pip wheel
|
||||
- run: python -m pip install .
|
||||
- name: Prepare site-packages
|
||||
run: |
|
||||
|
|
@ -86,13 +87,13 @@ jobs:
|
|||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.13'
|
||||
python-version: '3.14'
|
||||
cache: 'pip'
|
||||
cache-dependency-path: 'tests/requirements/py3.txt'
|
||||
- name: Install libmemcached-dev for pylibmc
|
||||
run: sudo apt-get install libmemcached-dev
|
||||
- name: Install and upgrade packaging tools
|
||||
run: python -m pip install --upgrade pip setuptools wheel
|
||||
run: python -m pip install --upgrade pip wheel
|
||||
- run: python -m pip install -r tests/requirements/py3.txt -e .
|
||||
- name: Run Selenium tests
|
||||
working-directory: ./tests/
|
||||
|
|
@ -104,7 +105,7 @@ jobs:
|
|||
name: Selenium tests, PostgreSQL
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:14-alpine
|
||||
image: postgres:15-alpine
|
||||
env:
|
||||
POSTGRES_DB: django
|
||||
POSTGRES_USER: user
|
||||
|
|
@ -122,13 +123,13 @@ jobs:
|
|||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.13'
|
||||
python-version: '3.14'
|
||||
cache: 'pip'
|
||||
cache-dependency-path: 'tests/requirements/py3.txt'
|
||||
- name: Install libmemcached-dev for pylibmc
|
||||
run: sudo apt-get install libmemcached-dev
|
||||
- name: Install and upgrade packaging tools
|
||||
run: python -m pip install --upgrade pip setuptools wheel
|
||||
run: python -m pip install --upgrade pip wheel
|
||||
- run: python -m pip install -r tests/requirements/py3.txt -r tests/requirements/postgres.txt -e .
|
||||
- name: Create PostgreSQL settings file
|
||||
run: mv ./.github/workflows/data/test_postgres.py.tpl ./tests/test_postgres.py
|
||||
|
|
@ -141,7 +142,7 @@ jobs:
|
|||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
version: [16, 17]
|
||||
version: [16, 17, 18]
|
||||
server_side_bindings: [0, 1]
|
||||
runs-on: ubuntu-latest
|
||||
name: PostgreSQL Versions
|
||||
|
|
@ -167,13 +168,13 @@ jobs:
|
|||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.13'
|
||||
python-version: '3.14'
|
||||
cache: 'pip'
|
||||
cache-dependency-path: 'tests/requirements/py3.txt'
|
||||
- name: Install libmemcached-dev for pylibmc
|
||||
run: sudo apt-get install libmemcached-dev
|
||||
- name: Install and upgrade packaging tools
|
||||
run: python -m pip install --upgrade pip setuptools wheel
|
||||
run: python -m pip install --upgrade pip wheel
|
||||
- run: python -m pip install -r tests/requirements/py3.txt -r tests/requirements/postgres.txt -e .
|
||||
- name: Create PostgreSQL settings file
|
||||
run: mv ./.github/workflows/data/test_postgres.py.tpl ./tests/test_postgres.py
|
||||
|
|
|
|||
4
.github/workflows/screenshots.yml
vendored
4
.github/workflows/screenshots.yml
vendored
|
|
@ -24,11 +24,11 @@ jobs:
|
|||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.13'
|
||||
python-version: '3.14'
|
||||
cache: 'pip'
|
||||
cache-dependency-path: 'tests/requirements/py3.txt'
|
||||
- name: Install and upgrade packaging tools
|
||||
run: python -m pip install --upgrade pip setuptools wheel
|
||||
run: python -m pip install --upgrade pip wheel
|
||||
- run: python -m pip install -r tests/requirements/py3.txt -e .
|
||||
|
||||
- name: Run Selenium tests with screenshots
|
||||
|
|
|
|||
10
.github/workflows/selenium.yml
vendored
10
.github/workflows/selenium.yml
vendored
|
|
@ -24,13 +24,13 @@ jobs:
|
|||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.13'
|
||||
python-version: '3.14'
|
||||
cache: 'pip'
|
||||
cache-dependency-path: 'tests/requirements/py3.txt'
|
||||
- name: Install libmemcached-dev for pylibmc
|
||||
run: sudo apt-get install libmemcached-dev
|
||||
- name: Install and upgrade packaging tools
|
||||
run: python -m pip install --upgrade pip setuptools wheel
|
||||
run: python -m pip install --upgrade pip wheel
|
||||
- run: python -m pip install -r tests/requirements/py3.txt -e .
|
||||
- name: Run Selenium tests
|
||||
working-directory: ./tests/
|
||||
|
|
@ -43,7 +43,7 @@ jobs:
|
|||
name: PostgreSQL
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:14-alpine
|
||||
image: postgres:15-alpine
|
||||
env:
|
||||
POSTGRES_DB: django
|
||||
POSTGRES_USER: user
|
||||
|
|
@ -61,13 +61,13 @@ jobs:
|
|||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.13'
|
||||
python-version: '3.14'
|
||||
cache: 'pip'
|
||||
cache-dependency-path: 'tests/requirements/py3.txt'
|
||||
- name: Install libmemcached-dev for pylibmc
|
||||
run: sudo apt-get install libmemcached-dev
|
||||
- name: Install and upgrade packaging tools
|
||||
run: python -m pip install --upgrade pip setuptools wheel
|
||||
run: python -m pip install --upgrade pip wheel
|
||||
- run: python -m pip install -r tests/requirements/py3.txt -r tests/requirements/postgres.txt -e .
|
||||
- name: Create PostgreSQL settings file
|
||||
run: mv ./.github/workflows/data/test_postgres.py.tpl ./tests/test_postgres.py
|
||||
|
|
|
|||
4
.github/workflows/tests.yml
vendored
4
.github/workflows/tests.yml
vendored
|
|
@ -23,7 +23,7 @@ jobs:
|
|||
strategy:
|
||||
matrix:
|
||||
python-version:
|
||||
- '3.13'
|
||||
- '3.14'
|
||||
name: Windows, SQLite, Python ${{ matrix.python-version }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
|
@ -35,7 +35,7 @@ jobs:
|
|||
cache: 'pip'
|
||||
cache-dependency-path: 'tests/requirements/py3.txt'
|
||||
- name: Install and upgrade packaging tools
|
||||
run: python -m pip install --upgrade pip setuptools wheel
|
||||
run: python -m pip install --upgrade pip wheel
|
||||
- run: python -m pip install -r tests/requirements/py3.txt -e .
|
||||
- name: Run tests
|
||||
run: python -Wall tests/runtests.py -v2
|
||||
|
|
|
|||
|
|
@ -1,26 +1,26 @@
|
|||
repos:
|
||||
- repo: https://github.com/psf/black-pre-commit-mirror
|
||||
rev: 25.1.0
|
||||
rev: 25.9.0
|
||||
hooks:
|
||||
- id: black
|
||||
exclude: \.py-tpl$
|
||||
- repo: https://github.com/adamchainz/blacken-docs
|
||||
rev: 1.19.1
|
||||
rev: 1.20.0
|
||||
hooks:
|
||||
- id: blacken-docs
|
||||
additional_dependencies:
|
||||
- black==25.1.0
|
||||
- black==25.9.0
|
||||
files: 'docs/.*\.txt$'
|
||||
args: ["--rst-literal-block"]
|
||||
- repo: https://github.com/PyCQA/isort
|
||||
rev: 5.13.2
|
||||
rev: 7.0.0
|
||||
hooks:
|
||||
- id: isort
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 7.2.0
|
||||
rev: 7.3.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
- repo: https://github.com/pre-commit/mirrors-eslint
|
||||
rev: v9.24.0
|
||||
rev: v9.36.0
|
||||
hooks:
|
||||
- id: eslint
|
||||
|
|
|
|||
5
AUTHORS
5
AUTHORS
|
|
@ -97,6 +97,7 @@ answer newbie questions, and generally made Django that much better:
|
|||
Andy Dustman <farcepest@gmail.com>
|
||||
Andy Gayton <andy-django@thecablelounge.com>
|
||||
andy@jadedplanet.net
|
||||
Annabelle Wiegart
|
||||
Anssi Kääriäinen <akaariai@gmail.com>
|
||||
ant9000@netwise.it
|
||||
Anthony Briggs <anthony.briggs@gmail.com>
|
||||
|
|
@ -209,6 +210,7 @@ answer newbie questions, and generally made Django that much better:
|
|||
Carlton Gibson <carlton.gibson@noumenal.es>
|
||||
cedric@terramater.net
|
||||
Chad Whitman <chad.whitman@icloud.com>
|
||||
Chaitanya Keyal <chaitanyakeyal@gmail.com>
|
||||
ChaosKCW
|
||||
Charlie Leifer <coleifer@gmail.com>
|
||||
charly.wilhelm@gmail.com
|
||||
|
|
@ -705,6 +707,7 @@ answer newbie questions, and generally made Django that much better:
|
|||
Matt Dennenbaum
|
||||
Matthew Flanagan <https://wadofstuff.blogspot.com/>
|
||||
Matthew Schinckel <matt@schinckel.net>
|
||||
Matthew Shirley <matt@mattshirley.net>
|
||||
Matthew Somerville <matthew-django@dracos.co.uk>
|
||||
Matthew Tretter <m@tthewwithanm.com>
|
||||
Matthew Wilkes <matt@matthewwilkes.name>
|
||||
|
|
@ -790,6 +793,7 @@ answer newbie questions, and generally made Django that much better:
|
|||
Nick Presta <nick@nickpresta.ca>
|
||||
Nick Sandford <nick.sandford@gmail.com>
|
||||
Nick Sarbicki <nick.a.sarbicki@gmail.com>
|
||||
Nick Stefan <https://github.com/nickstefan>
|
||||
Niclas Olofsson <n@niclasolofsson.se>
|
||||
Nicola Larosa <nico@teknico.net>
|
||||
Nicolas Lara <nicolaslara@gmail.com>
|
||||
|
|
@ -1062,6 +1066,7 @@ answer newbie questions, and generally made Django that much better:
|
|||
Unai Zalakain <unai@gisa-elkartea.org>
|
||||
Valentina Mukhamedzhanova <umirra@gmail.com>
|
||||
valtron
|
||||
Varun Kasyap Pentamaraju <varunkasyap@hotmail.com>
|
||||
Vasiliy Stavenko <stavenko@gmail.com>
|
||||
Vasil Vangelovski
|
||||
Vibhu Agarwal <vibhu-agarwal.github.io>
|
||||
|
|
|
|||
|
|
@ -92,6 +92,7 @@ LANGUAGES = [
|
|||
("hi", gettext_noop("Hindi")),
|
||||
("hr", gettext_noop("Croatian")),
|
||||
("hsb", gettext_noop("Upper Sorbian")),
|
||||
("ht", gettext_noop("Haitian Creole")),
|
||||
("hu", gettext_noop("Hungarian")),
|
||||
("hy", gettext_noop("Armenian")),
|
||||
("ia", gettext_noop("Interlingua")),
|
||||
|
|
|
|||
|
|
@ -255,6 +255,12 @@ LANG_INFO = {
|
|||
"name": "Upper Sorbian",
|
||||
"name_local": "hornjoserbsce",
|
||||
},
|
||||
"ht": {
|
||||
"bidi": False,
|
||||
"code": "ht",
|
||||
"name": "Haitian Creole",
|
||||
"name_local": "Kreyòl Ayisyen",
|
||||
},
|
||||
"hu": {
|
||||
"bidi": False,
|
||||
"code": "hu",
|
||||
|
|
|
|||
|
|
@ -25,11 +25,9 @@ DATETIME_INPUT_FORMATS = [
|
|||
"%d.%m.%Y %H:%M", # '25.10.2006 14:30'
|
||||
]
|
||||
|
||||
# these are the separators for non-monetary numbers. For monetary numbers,
|
||||
# the DECIMAL_SEPARATOR is a . (decimal point) and the THOUSAND_SEPARATOR is a
|
||||
# ' (single quote).
|
||||
# For details, please refer to the documentation and the following link:
|
||||
# https://www.bk.admin.ch/bk/de/home/dokumentation/sprachen/hilfsmittel-textredaktion/schreibweisungen.html
|
||||
# Swiss number formatting can vary based on context (e.g. Fr. 23.50 vs 22,5 m).
|
||||
# Django does not support context-specific formatting and uses generic
|
||||
# separators.
|
||||
DECIMAL_SEPARATOR = ","
|
||||
THOUSAND_SEPARATOR = "\xa0" # non-breaking space
|
||||
NUMBER_GROUPING = 3
|
||||
|
|
|
|||
|
|
@ -178,6 +178,10 @@ msgstr ""
|
|||
msgid "Upper Sorbian"
|
||||
msgstr ""
|
||||
|
||||
#: conf/global_settings.py:95
|
||||
msgid "Haitian Creole"
|
||||
msgstr ""
|
||||
|
||||
#: conf/global_settings.py:95
|
||||
msgid "Hungarian"
|
||||
msgstr ""
|
||||
|
|
|
|||
|
|
@ -3,12 +3,12 @@
|
|||
# The *_FORMAT strings use the Django date format syntax,
|
||||
# see https://docs.djangoproject.com/en/dev/ref/templates/builtins/#date
|
||||
DATE_FORMAT = "j F Y" # 31 janvier 2024
|
||||
TIME_FORMAT = "H\xa0h\xa0i" # 13 h 40
|
||||
DATETIME_FORMAT = "j F Y, H\xa0h\xa0i" # 31 janvier 2024, 13 h 40
|
||||
TIME_FORMAT = "H\xa0\\h\xa0i" # 13 h 40
|
||||
DATETIME_FORMAT = "j F Y, H\xa0\\h\xa0i" # 31 janvier 2024, 13 h 40
|
||||
YEAR_MONTH_FORMAT = "F Y"
|
||||
MONTH_DAY_FORMAT = "j F"
|
||||
SHORT_DATE_FORMAT = "Y-m-d"
|
||||
SHORT_DATETIME_FORMAT = "Y-m-d H\xa0h\xa0i"
|
||||
SHORT_DATETIME_FORMAT = "Y-m-d H\xa0\\h\xa0i"
|
||||
FIRST_DAY_OF_WEEK = 0 # Dimanche
|
||||
|
||||
# The *_INPUT_FORMATS strings use the Python strftime format syntax,
|
||||
|
|
|
|||
|
|
@ -27,6 +27,10 @@ DATETIME_INPUT_FORMATS = [
|
|||
"%d/%m/%Y %H:%M:%S.%f", # '25/10/2006 14:30:59.000200'
|
||||
"%d/%m/%Y %H:%M", # '25/10/2006 14:30'
|
||||
]
|
||||
|
||||
# Swiss number formatting can vary based on context (e.g. Fr. 23.50 vs 22,5 m).
|
||||
# Django does not support context-specific formatting and uses generic
|
||||
# separators.
|
||||
DECIMAL_SEPARATOR = ","
|
||||
THOUSAND_SEPARATOR = "\xa0" # non-breaking space
|
||||
NUMBER_GROUPING = 3
|
||||
|
|
|
|||
0
django/conf/locale/ht/__init__.py
Normal file
0
django/conf/locale/ht/__init__.py
Normal file
48
django/conf/locale/ht/formats.py
Normal file
48
django/conf/locale/ht/formats.py
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
# This file is distributed under the same license as the Django package.
|
||||
#
|
||||
# The *_FORMAT strings use the Django date format syntax,
|
||||
# see https://docs.djangoproject.com/en/dev/ref/templates/builtins/#date
|
||||
DATE_FORMAT = "N j, Y"
|
||||
TIME_FORMAT = "P"
|
||||
DATETIME_FORMAT = "N j, Y, P"
|
||||
YEAR_MONTH_FORMAT = "F Y"
|
||||
MONTH_DAY_FORMAT = "F j"
|
||||
SHORT_DATE_FORMAT = "d/m/Y"
|
||||
SHORT_DATETIME_FORMAT = "d/m/Y P"
|
||||
FIRST_DAY_OF_WEEK = 0
|
||||
|
||||
# The *_INPUT_FORMATS strings use the Python strftime format syntax,
|
||||
# see https://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
DATE_INPUT_FORMATS = [
|
||||
"%Y-%m-%d", # '2006-10-25'
|
||||
"%m/%d/%Y", # '10/25/2006'
|
||||
"%m/%d/%y", # '10/25/06'
|
||||
"%b %d %Y", # 'Oct 25 2006'
|
||||
"%b %d, %Y", # 'Oct 25, 2006'
|
||||
"%d %b %Y", # '25 Oct 2006'
|
||||
"%d %b, %Y", # '25 Oct, 2006'
|
||||
"%B %d %Y", # 'October 25 2006'
|
||||
"%B %d, %Y", # 'October 25, 2006'
|
||||
"%d %B %Y", # '25 October 2006'
|
||||
"%d %B, %Y", # '25 October, 2006'
|
||||
]
|
||||
DATETIME_INPUT_FORMATS = [
|
||||
"%Y-%m-%d %H:%M:%S", # '2006-10-25 14:30:59'
|
||||
"%Y-%m-%d %H:%M:%S.%f", # '2006-10-25 14:30:59.000200'
|
||||
"%Y-%m-%d %H:%M", # '2006-10-25 14:30'
|
||||
"%m/%d/%Y %H:%M:%S", # '10/25/2006 14:30:59'
|
||||
"%m/%d/%Y %H:%M:%S.%f", # '10/25/2006 14:30:59.000200'
|
||||
"%m/%d/%Y %H:%M", # '10/25/2006 14:30'
|
||||
"%m/%d/%y %H:%M:%S", # '10/25/06 14:30:59'
|
||||
"%m/%d/%y %H:%M:%S.%f", # '10/25/06 14:30:59.000200'
|
||||
"%m/%d/%y %H:%M", # '10/25/06 14:30'
|
||||
]
|
||||
TIME_INPUT_FORMATS = [
|
||||
"%H:%M:%S", # '14:30:59'
|
||||
"%H:%M:%S.%f", # '14:30:59.000200'
|
||||
"%H:%M", # '14:30'
|
||||
]
|
||||
|
||||
DECIMAL_SEPARATOR = ","
|
||||
THOUSAND_SEPARATOR = "\xa0"
|
||||
NUMBER_GROUPING = 3
|
||||
|
|
@ -754,19 +754,37 @@ td ul.errorlist + input, td ul.errorlist + select, td ul.errorlist + textarea {
|
|||
|
||||
/* BREADCRUMBS */
|
||||
|
||||
div.breadcrumbs {
|
||||
ol.breadcrumbs {
|
||||
background: var(--breadcrumbs-bg);
|
||||
padding: 10px 40px;
|
||||
border: none;
|
||||
color: var(--breadcrumbs-fg);
|
||||
text-align: left;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
div.breadcrumbs a {
|
||||
ol.breadcrumbs li {
|
||||
display: inline-block;
|
||||
font-size: 0.875rem;
|
||||
padding: 0;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
ol.breadcrumbs li:not([aria-current="page"])::after {
|
||||
content: ' \203A ' / '';
|
||||
}
|
||||
|
||||
ol.breadcrumbs li a[aria-current="page"] {
|
||||
color: var(--breadcrumbs-fg);
|
||||
text-decoration: none !important;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
ol.breadcrumbs a {
|
||||
color: var(--breadcrumbs-link-fg);
|
||||
}
|
||||
|
||||
div.breadcrumbs a:focus, div.breadcrumbs a:hover {
|
||||
ol.breadcrumbs a:focus, ol.breadcrumbs a:hover {
|
||||
color: var(--breadcrumbs-fg);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -357,7 +357,7 @@ body.popup .submit-row {
|
|||
width: 48em;
|
||||
}
|
||||
|
||||
.flatpages-flatpage #id_content {
|
||||
.app-flatpages.model-flatpage #id_content {
|
||||
height: 40.2em;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@
|
|||
{% load i18n %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<div class="breadcrumbs">
|
||||
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
|
||||
› {% translate 'Server error' %}
|
||||
</div>
|
||||
<ol class="breadcrumbs">
|
||||
<li><a href="{% url 'admin:index' %}">{% translate 'Home' %}</a></li>
|
||||
<li aria-current="page">{% translate 'Server error' %}</li>
|
||||
</ol>
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}{% translate 'Server error (500)' %}{% endblock %}
|
||||
|
|
|
|||
|
|
@ -6,13 +6,13 @@
|
|||
{% if not is_popup %}
|
||||
{% block nav-breadcrumbs %}
|
||||
<nav aria-label="{% translate 'Breadcrumbs' %}">
|
||||
<div class="breadcrumbs">
|
||||
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
|
||||
›
|
||||
{% for app in app_list %}
|
||||
{{ app.name }}
|
||||
<ol class="breadcrumbs">
|
||||
<li><a href="{% url 'admin:index' %}">{% translate 'Home' %}</a></li>
|
||||
<li aria-current="page">
|
||||
{% for app in app_list %}{{ app.name }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
{% endblock %}
|
||||
{% endif %}
|
||||
|
|
|
|||
|
|
@ -11,13 +11,13 @@
|
|||
{% block bodyclass %}{{ block.super }} {{ opts.app_label }}-{{ opts.model_name }} change-form{% endblock %}
|
||||
{% if not is_popup %}
|
||||
{% block breadcrumbs %}
|
||||
<div class="breadcrumbs">
|
||||
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
|
||||
› <a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a>
|
||||
› <a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>
|
||||
› <a href="{% url opts|admin_urlname:'change' original.pk|admin_urlquote %}">{{ original|truncatewords:"18" }}</a>
|
||||
› {% if form.user.has_usable_password %}{% translate 'Change password' %}{% else %}{% translate 'Set password' %}{% endif %}
|
||||
</div>
|
||||
<ol class="breadcrumbs">
|
||||
<li><a href="{% url 'admin:index' %}">{% translate 'Home' %}</a></li>
|
||||
<li><a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a></li>
|
||||
<li><a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a></li>
|
||||
<li><a href="{% url opts|admin_urlname:'change' original.pk|admin_urlquote %}">{{ original|truncatewords:"18" }}</a></li>
|
||||
<li aria-current="page">{% if form.user.has_usable_password %}{% translate 'Change password' %}{% else %}{% translate 'Set password' %}{% endif %}</li>
|
||||
</ol>
|
||||
{% endblock %}
|
||||
{% endif %}
|
||||
{% block content %}<div id="content-main">
|
||||
|
|
|
|||
|
|
@ -72,10 +72,10 @@
|
|||
{% block nav-breadcrumbs %}
|
||||
<nav aria-label="{% translate 'Breadcrumbs' %}">
|
||||
{% block breadcrumbs %}
|
||||
<div class="breadcrumbs">
|
||||
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
|
||||
{% if title %} › {{ title }}{% endif %}
|
||||
</div>
|
||||
<ol class="breadcrumbs">
|
||||
<li><a href="{% url 'admin:index' %}">{% translate 'Home' %}</a></li>
|
||||
{% if title %}<li aria-current="page">{{ title }}</li>{% endif %}
|
||||
</ol>
|
||||
{% endblock %}
|
||||
</nav>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -15,12 +15,12 @@
|
|||
|
||||
{% if not is_popup %}
|
||||
{% block breadcrumbs %}
|
||||
<div class="breadcrumbs">
|
||||
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
|
||||
› <a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a>
|
||||
› {% if has_view_permission %}<a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>{% else %}{{ opts.verbose_name_plural|capfirst }}{% endif %}
|
||||
› {% if add %}{% blocktranslate with name=opts.verbose_name %}Add {{ name }}{% endblocktranslate %}{% else %}{{ original|truncatewords:"18" }}{% endif %}
|
||||
</div>
|
||||
<ol class="breadcrumbs">
|
||||
<li><a href="{% url 'admin:index' %}">{% translate 'Home' %}</a></li>
|
||||
<li><a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a></li>
|
||||
<li>{% if has_view_permission %}<a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>{% else %}{{ opts.verbose_name_plural|capfirst }}{% endif %}</li>
|
||||
<li aria-current="page">{% if add %}{% blocktranslate with name=opts.verbose_name %}Add {{ name }}{% endblocktranslate %}{% else %}{{ original|truncatewords:"18" }}{% endif %}</li>
|
||||
</ol>
|
||||
{% endblock %}
|
||||
{% endif %}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,11 +29,11 @@
|
|||
|
||||
{% if not is_popup %}
|
||||
{% block breadcrumbs %}
|
||||
<div class="breadcrumbs">
|
||||
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
|
||||
› <a href="{% url 'admin:app_list' app_label=cl.opts.app_label %}">{{ cl.opts.app_config.verbose_name }}</a>
|
||||
› {{ cl.opts.verbose_name_plural|capfirst }}
|
||||
</div>
|
||||
<ol class="breadcrumbs">
|
||||
<li><a href="{% url 'admin:index' %}">{% translate 'Home' %}</a></li>
|
||||
<li><a href="{% url 'admin:app_list' app_label=cl.opts.app_label %}">{{ cl.opts.app_config.verbose_name }}</a></li>
|
||||
<li aria-current="page">{{ cl.opts.verbose_name_plural|capfirst }}</li>
|
||||
</ol>
|
||||
{% endblock %}
|
||||
{% endif %}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,13 +10,13 @@
|
|||
{% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} delete-confirmation{% endblock %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<div class="breadcrumbs">
|
||||
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
|
||||
› <a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a>
|
||||
› <a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>
|
||||
› <a href="{% url opts|admin_urlname:'change' object.pk|admin_urlquote %}">{{ object|truncatewords:"18" }}</a>
|
||||
› {% translate 'Delete' %}
|
||||
</div>
|
||||
<ol class="breadcrumbs">
|
||||
<li><a href="{% url 'admin:index' %}">{% translate 'Home' %}</a></li>
|
||||
<li><a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a></li>
|
||||
<li><a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a></li>
|
||||
<li><a href="{% url opts|admin_urlname:'change' object.pk|admin_urlquote %}">{{ object|truncatewords:"18" }}</a></li>
|
||||
<li aria-current="page">{% translate 'Delete' %}</li>
|
||||
</ol>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
|
|
|||
|
|
@ -10,12 +10,12 @@
|
|||
{% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} delete-confirmation delete-selected-confirmation{% endblock %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<div class="breadcrumbs">
|
||||
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
|
||||
› <a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a>
|
||||
› <a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>
|
||||
› {% translate 'Delete multiple objects' %}
|
||||
</div>
|
||||
<ol class="breadcrumbs">
|
||||
<li><a href="{% url 'admin:index' %}">{% translate 'Home' %}</a></li>
|
||||
<li><a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a></li>
|
||||
<li><a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a></li>
|
||||
<li aria-current="page">{% translate 'Delete multiple objects' %}</li>
|
||||
</ol>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@
|
|||
{% load i18n %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<div class="breadcrumbs">
|
||||
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
|
||||
› {{ title }}
|
||||
</div>
|
||||
<ol class="breadcrumbs">
|
||||
<li><a href="{% url 'admin:index' %}">{% translate 'Home' %}</a></li>
|
||||
<li aria-current="page">{{ title }}</li>
|
||||
</ol>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@
|
|||
{% load i18n admin_urls %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<div class="breadcrumbs">
|
||||
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
|
||||
› <a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a>
|
||||
› <a href="{% url opts|admin_urlname:'changelist' %}">{{ module_name }}</a>
|
||||
› <a href="{% url opts|admin_urlname:'change' object.pk|admin_urlquote %}">{{ object|truncatewords:"18" }}</a>
|
||||
› {% translate 'History' %}
|
||||
</div>
|
||||
<ol class="breadcrumbs">
|
||||
<li><a href="{% url 'admin:index' %}">{% translate 'Home' %}</a></li>
|
||||
<li><a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a></li>
|
||||
<li><a href="{% url opts|admin_urlname:'changelist' %}">{{ module_name }}</a></li>
|
||||
<li><a href="{% url opts|admin_urlname:'change' object.pk|admin_urlquote %}">{{ object|truncatewords:"18" }}</a></li>
|
||||
<li aria-current="page">{% translate 'History' %}</li>
|
||||
</ol>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
{% extends "admin/base_site.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block breadcrumbs %}<div class="breadcrumbs"><a href="{% url 'admin:index' %}">{% translate 'Home' %}</a></div>{% endblock %}
|
||||
{% block breadcrumbs %}
|
||||
<ol class="breadcrumbs">
|
||||
<li><a href="{% url 'admin:index' %}">{% translate 'Home' %}</a></li>
|
||||
<li aria-current="page">{% translate 'Logout' %}</li>
|
||||
</ol>
|
||||
{% endblock %}
|
||||
|
||||
{% block nav-sidebar %}{% endblock %}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,10 +9,10 @@
|
|||
{% include "admin/color_theme_toggle.html" %}
|
||||
{% endblock %}
|
||||
{% block breadcrumbs %}
|
||||
<div class="breadcrumbs">
|
||||
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
|
||||
› {% translate 'Password change' %}
|
||||
</div>
|
||||
<ol class="breadcrumbs">
|
||||
<li><a href="{% url 'admin:index' %}">{% translate 'Home' %}</a></li>
|
||||
<li aria-current="page">{% translate 'Password change' %}</li>
|
||||
</ol>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
|
|
|||
|
|
@ -12,10 +12,10 @@
|
|||
{% include "admin/color_theme_toggle.html" %}
|
||||
{% endblock %}
|
||||
{% block breadcrumbs %}
|
||||
<div class="breadcrumbs">
|
||||
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
|
||||
› {% translate 'Password change' %}
|
||||
</div>
|
||||
<ol class="breadcrumbs">
|
||||
<li><a href="{% url 'admin:index' %}">{% translate 'Home' %}</a></li>
|
||||
<li aria-current="page">{% translate 'Password change' %}</li>
|
||||
</ol>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}<div id="content-main">
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@
|
|||
{% load i18n %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<div class="breadcrumbs">
|
||||
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
|
||||
› {% translate 'Password reset' %}
|
||||
</div>
|
||||
<ol class="breadcrumbs">
|
||||
<li><a href="{% url 'admin:index' %}">{% translate 'Home' %}</a></li>
|
||||
<li aria-current="page">{% translate 'Password reset' %}</li>
|
||||
</ol>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@
|
|||
{% block title %}{% if form.new_password1.errors or form.new_password2.errors %}{% translate "Error:" %} {% endif %}{{ block.super }}{% endblock %}
|
||||
{% block extrastyle %}{{ block.super }}<link rel="stylesheet" href="{% static "admin/css/forms.css" %}">{% endblock %}
|
||||
{% block breadcrumbs %}
|
||||
<div class="breadcrumbs">
|
||||
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
|
||||
› {% translate 'Password reset confirmation' %}
|
||||
</div>
|
||||
<ol class="breadcrumbs">
|
||||
<li><a href="{% url 'admin:index' %}">{% translate 'Home' %}</a></li>
|
||||
<li aria-current="page">{% translate 'Password reset confirmation' %}</li>
|
||||
</ol>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@
|
|||
{% load i18n %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<div class="breadcrumbs">
|
||||
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
|
||||
› {% translate 'Password reset' %}
|
||||
</div>
|
||||
<ol class="breadcrumbs">
|
||||
<li><a href="{% url 'admin:index' %}">{% translate 'Home' %}</a></li>
|
||||
<li aria-current="page">{% translate 'Password reset' %}</li>
|
||||
</ol>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@
|
|||
{% block title %}{% if form.email.errors %}{% translate "Error:" %} {% endif %}{{ block.super }}{% endblock %}
|
||||
{% block extrastyle %}{{ block.super }}<link rel="stylesheet" href="{% static "admin/css/forms.css" %}">{% endblock %}
|
||||
{% block breadcrumbs %}
|
||||
<div class="breadcrumbs">
|
||||
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
|
||||
› {% translate 'Password reset' %}
|
||||
</div>
|
||||
<ol class="breadcrumbs">
|
||||
<li><a href="{% url 'admin:index' %}">{% translate 'Home' %}</a></li>
|
||||
<li aria-current="page">{% translate 'Password reset' %}</li>
|
||||
</ol>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
|
|
|||
|
|
@ -184,8 +184,8 @@ def get_deleted_objects(objs, request, admin_site):
|
|||
|
||||
|
||||
class NestedObjects(Collector):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
def __init__(self, *args, force_collection=True, **kwargs):
|
||||
super().__init__(*args, force_collection=force_collection, **kwargs)
|
||||
self.edges = {} # {from_instance: [to_instances]}
|
||||
self.protected = set()
|
||||
self.model_objs = defaultdict(set)
|
||||
|
|
@ -242,13 +242,6 @@ class NestedObjects(Collector):
|
|||
roots.extend(self._nested(root, seen, format_callback))
|
||||
return roots
|
||||
|
||||
def can_fast_delete(self, *args, **kwargs):
|
||||
"""
|
||||
We always want to load the objects into memory so that we can display
|
||||
them to the user in confirm page.
|
||||
"""
|
||||
return False
|
||||
|
||||
|
||||
def model_format_dict(obj):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@
|
|||
{% load i18n %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<div class="breadcrumbs">
|
||||
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
|
||||
› <a href="{% url 'django-admindocs-docroot' %}">{% translate 'Documentation' %}</a>
|
||||
› {% translate 'Bookmarklets' %}
|
||||
</div>
|
||||
<ol class="breadcrumbs">
|
||||
<li><a href="{% url 'admin:index' %}">{% translate 'Home' %}</a></li>
|
||||
<li><a href="{% url 'django-admindocs-docroot' %}">{% translate 'Documentation' %}</a></li>
|
||||
<li aria-current="page">{% translate 'Bookmarklets' %}</li>
|
||||
</ol>
|
||||
{% endblock %}
|
||||
{% block title %}{% translate "Documentation bookmarklets" %}{% endblock %}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@
|
|||
{% load i18n %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<div class="breadcrumbs">
|
||||
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
|
||||
› {% translate 'Documentation' %}
|
||||
</div>
|
||||
<ol class="breadcrumbs">
|
||||
<li><a href="{% url 'admin:index' %}">{% translate 'Home' %}</a></li>
|
||||
<li aria-current="page">{% translate 'Documentation' %}</li>
|
||||
</ol>
|
||||
{% endblock %}
|
||||
{% block title %}{% translate 'Documentation' %}{% endblock %}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@
|
|||
{% load i18n %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<div class="breadcrumbs">
|
||||
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
|
||||
› {% translate 'Documentation' %}
|
||||
</div>
|
||||
<ol class="breadcrumbs">
|
||||
<li><a href="{% url 'admin:index' %}">{% translate 'Home' %}</a></li>
|
||||
<li aria-current="page">{% translate 'Documentation' %}</li>
|
||||
</ol>
|
||||
{% endblock %}
|
||||
{% block title %}{% translate 'Please install docutils' %}{% endblock %}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,12 +10,12 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<div class="breadcrumbs">
|
||||
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
|
||||
› <a href="{% url 'django-admindocs-docroot' %}">{% translate 'Documentation' %}</a>
|
||||
› <a href="{% url 'django-admindocs-models-index' %}">{% translate 'Models' %}</a>
|
||||
› {{ name }}
|
||||
</div>
|
||||
<ol class="breadcrumbs">
|
||||
<li><a href="{% url 'admin:index' %}">{% translate 'Home' %}</a></li>
|
||||
<li><a href="{% url 'django-admindocs-docroot' %}">{% translate 'Documentation' %}</a></li>
|
||||
<li><a href="{% url 'django-admindocs-models-index' %}">{% translate 'Models' %}</a></li>
|
||||
<li aria-current="page">{{ name }}</li>
|
||||
</ol>
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}{% blocktranslate %}Model: {{ name }}{% endblocktranslate %}{% endblock %}
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@
|
|||
{% block coltype %}colSM{% endblock %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<div class="breadcrumbs">
|
||||
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
|
||||
› <a href="{% url 'django-admindocs-docroot' %}">{% translate 'Documentation' %}</a>
|
||||
› {% translate 'Models' %}
|
||||
</div>
|
||||
<ol class="breadcrumbs">
|
||||
<li><a href="{% url 'admin:index' %}">{% translate 'Home' %}</a></li>
|
||||
<li><a href="{% url 'django-admindocs-docroot' %}">{% translate 'Documentation' %}</a><li>
|
||||
<li aria-current="page">{% translate 'Models' %}</li>
|
||||
</ol>
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}{% translate 'Models' %}{% endblock %}
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@
|
|||
{% load i18n %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<div class="breadcrumbs">
|
||||
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
|
||||
› <a href="{% url 'django-admindocs-docroot' %}">{% translate 'Documentation' %}</a>
|
||||
› {% translate 'Templates' %}
|
||||
› {{ name }}
|
||||
</div>
|
||||
<ol class="breadcrumbs">
|
||||
<li><a href="{% url 'admin:index' %}">{% translate 'Home' %}</a></li>
|
||||
<li><a href="{% url 'django-admindocs-docroot' %}">{% translate 'Documentation' %}</a></li>
|
||||
<li>{% translate 'Templates' %}</li>
|
||||
<li aria-current="page">{{ name }}</li>
|
||||
</ol>
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}{% blocktranslate %}Template: {{ name }}{% endblocktranslate %}{% endblock %}
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@
|
|||
|
||||
{% block coltype %}colSM{% endblock %}
|
||||
{% block breadcrumbs %}
|
||||
<div class="breadcrumbs">
|
||||
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
|
||||
› <a href="{% url 'django-admindocs-docroot' %}">{% translate 'Documentation' %}</a>
|
||||
› {% translate 'Filters' %}
|
||||
</div>
|
||||
<ol class="breadcrumbs">
|
||||
<li><a href="{% url 'admin:index' %}">{% translate 'Home' %}</a></li>
|
||||
<li><a href="{% url 'django-admindocs-docroot' %}">{% translate 'Documentation' %}</a></li>
|
||||
<li aria-current="page">{% translate 'Filters' %}</li>
|
||||
</ol>
|
||||
{% endblock %}
|
||||
{% block title %}{% translate 'Template filters' %}{% endblock %}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@
|
|||
|
||||
{% block coltype %}colSM{% endblock %}
|
||||
{% block breadcrumbs %}
|
||||
<div class="breadcrumbs">
|
||||
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
|
||||
› <a href="{% url 'django-admindocs-docroot' %}">{% translate 'Documentation' %}</a>
|
||||
› {% translate 'Tags' %}
|
||||
</div>
|
||||
<ol class="breadcrumbs">
|
||||
<li><a href="{% url 'admin:index' %}">{% translate 'Home' %}</a></li>
|
||||
<li><a href="{% url 'django-admindocs-docroot' %}">{% translate 'Documentation' %}</a></li>
|
||||
<li aria-current="page">{% translate 'Tags' %}</li>
|
||||
</ol>
|
||||
{% endblock %}
|
||||
{% block title %}{% translate 'Template tags' %}{% endblock %}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@
|
|||
{% load i18n %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<div class="breadcrumbs">
|
||||
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
|
||||
› <a href="{% url 'django-admindocs-docroot' %}">{% translate 'Documentation' %}</a>
|
||||
› <a href="{% url 'django-admindocs-views-index' %}">{% translate 'Views' %}</a>
|
||||
› {{ name }}
|
||||
</div>
|
||||
<ol class="breadcrumbs">
|
||||
<li><a href="{% url 'admin:index' %}">{% translate 'Home' %}</a></li>
|
||||
<li><a href="{% url 'django-admindocs-docroot' %}">{% translate 'Documentation' %}</a></li>
|
||||
<li><a href="{% url 'django-admindocs-views-index' %}">{% translate 'Views' %}</a></li>
|
||||
<li aria-current="page">{{ name }}</li>
|
||||
</ol>
|
||||
{% endblock %}
|
||||
{% block title %}{% blocktranslate %}View: {{ name }}{% endblocktranslate %}{% endblock %}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@
|
|||
|
||||
{% block coltype %}colSM{% endblock %}
|
||||
{% block breadcrumbs %}
|
||||
<div class="breadcrumbs">
|
||||
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
|
||||
› <a href="{% url 'django-admindocs-docroot' %}">{% translate 'Documentation' %}</a>
|
||||
› {% translate 'Views' %}
|
||||
</div>
|
||||
<ol class="breadcrumbs">
|
||||
<li><a href="{% url 'admin:index' %}">{% translate 'Home' %}</a></li>
|
||||
<li><a href="{% url 'django-admindocs-docroot' %}">{% translate 'Documentation' %}</a></li>
|
||||
<li aria-current="page">{% translate 'Views' %}</li>
|
||||
</ol>
|
||||
{% endblock %}
|
||||
{% block title %}{% translate 'Views' %}{% endblock %}
|
||||
|
||||
|
|
|
|||
|
|
@ -134,6 +134,9 @@ class SetPasswordMixin:
|
|||
user.save()
|
||||
return user
|
||||
|
||||
def __class_getitem__(cls, *args, **kwargs):
|
||||
return cls
|
||||
|
||||
|
||||
class SetUnusablePasswordMixin:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -10,12 +10,14 @@ from django.core.exceptions import FieldDoesNotExist, ObjectDoesNotExist
|
|||
from django.db import DEFAULT_DB_ALIAS, models, router, transaction
|
||||
from django.db.models import DO_NOTHING, ForeignObject, ForeignObjectRel
|
||||
from django.db.models.base import ModelBase, make_foreign_order_accessors
|
||||
from django.db.models.deletion import DatabaseOnDelete
|
||||
from django.db.models.fields import Field
|
||||
from django.db.models.fields.mixins import FieldCacheMixin
|
||||
from django.db.models.fields.related import (
|
||||
ReverseManyToOneDescriptor,
|
||||
lazy_related_operation,
|
||||
)
|
||||
from django.db.models.query import prefetch_related_objects
|
||||
from django.db.models.query_utils import PathInfo
|
||||
from django.db.models.sql import AND
|
||||
from django.db.models.sql.where import WhereNode
|
||||
|
|
@ -138,6 +140,16 @@ class GenericForeignKey(FieldCacheMixin, Field):
|
|||
id="contenttypes.E004",
|
||||
)
|
||||
]
|
||||
elif isinstance(field.remote_field.on_delete, DatabaseOnDelete):
|
||||
return [
|
||||
checks.Error(
|
||||
f"'{self.model._meta.object_name}.{self.ct_field}' cannot use "
|
||||
"the database-level on_delete variant.",
|
||||
hint="Change the on_delete rule to the non-database variant.",
|
||||
obj=self,
|
||||
id="contenttypes.E006",
|
||||
)
|
||||
]
|
||||
else:
|
||||
return []
|
||||
|
||||
|
|
@ -200,11 +212,13 @@ class GenericForeignKeyDescriptor:
|
|||
for ct_id, fkeys in fk_dict.items():
|
||||
if ct_id in custom_queryset_dict:
|
||||
# Return values from the custom queryset, if provided.
|
||||
ret_val.extend(custom_queryset_dict[ct_id].filter(pk__in=fkeys))
|
||||
queryset = custom_queryset_dict[ct_id].filter(pk__in=fkeys)
|
||||
else:
|
||||
instance = instance_dict[ct_id]
|
||||
ct = self.field.get_content_type(id=ct_id, using=instance._state.db)
|
||||
ret_val.extend(ct.get_all_objects_for_this_type(pk__in=fkeys))
|
||||
queryset = ct.get_all_objects_for_this_type(pk__in=fkeys)
|
||||
|
||||
ret_val.extend(queryset.fetch_mode(instances[0]._state.fetch_mode))
|
||||
|
||||
# For doing the join in Python, we have to match both the FK val and
|
||||
# the content type, so we use a callable that returns a (fk, class)
|
||||
|
|
@ -253,6 +267,15 @@ class GenericForeignKeyDescriptor:
|
|||
return rel_obj
|
||||
else:
|
||||
rel_obj = None
|
||||
|
||||
instance._state.fetch_mode.fetch(self, instance)
|
||||
return self.field.get_cached_value(instance)
|
||||
|
||||
def fetch_one(self, instance):
|
||||
f = self.field.model._meta.get_field(self.field.ct_field)
|
||||
ct_id = getattr(instance, f.attname, None)
|
||||
pk_val = getattr(instance, self.field.fk_field)
|
||||
rel_obj = None
|
||||
if ct_id is not None:
|
||||
ct = self.field.get_content_type(id=ct_id, using=instance._state.db)
|
||||
try:
|
||||
|
|
@ -261,8 +284,14 @@ class GenericForeignKeyDescriptor:
|
|||
)
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
else:
|
||||
rel_obj._state.fetch_mode = instance._state.fetch_mode
|
||||
self.field.set_cached_value(instance, rel_obj)
|
||||
return rel_obj
|
||||
|
||||
def fetch_many(self, instances):
|
||||
is_cached = self.field.is_cached
|
||||
missing_instances = [i for i in instances if not is_cached(i)]
|
||||
return prefetch_related_objects(missing_instances, self.field.name)
|
||||
|
||||
def __set__(self, instance, value):
|
||||
ct = None
|
||||
|
|
@ -622,7 +651,11 @@ def create_generic_related_manager(superclass, rel):
|
|||
Filter the queryset for the instance this manager is bound to.
|
||||
"""
|
||||
db = self._db or router.db_for_read(self.model, instance=self.instance)
|
||||
return queryset.using(db).filter(**self.core_filters)
|
||||
return (
|
||||
queryset.using(db)
|
||||
.fetch_mode(self.instance._state.fetch_mode)
|
||||
.filter(**self.core_filters)
|
||||
)
|
||||
|
||||
def _remove_prefetched_objects(self):
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -61,7 +61,9 @@ class Command(BaseCommand):
|
|||
ct_info.append(
|
||||
" - Content type for %s.%s" % (ct.app_label, ct.model)
|
||||
)
|
||||
collector = NoFastDeleteCollector(using=using, origin=ct)
|
||||
collector = Collector(
|
||||
using=using, origin=ct, force_collection=True
|
||||
)
|
||||
collector.collect([ct])
|
||||
|
||||
for obj_type, objs in collector.data.items():
|
||||
|
|
@ -103,11 +105,3 @@ class Command(BaseCommand):
|
|||
else:
|
||||
if verbosity >= 2:
|
||||
self.stdout.write("Stale content types remain.")
|
||||
|
||||
|
||||
class NoFastDeleteCollector(Collector):
|
||||
def can_fast_delete(self, *args, **kwargs):
|
||||
"""
|
||||
Always load related objects to display them when showing confirmation.
|
||||
"""
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -76,8 +76,6 @@ class MySQLOperations(BaseSpatialOperations, DatabaseOperations):
|
|||
if is_mariadb:
|
||||
if self.connection.mysql_version < (12, 0, 1):
|
||||
disallowed_aggregates.insert(0, models.Collect)
|
||||
elif self.connection.mysql_version < (8, 0, 24):
|
||||
disallowed_aggregates.insert(0, models.Collect)
|
||||
return tuple(disallowed_aggregates)
|
||||
|
||||
function_names = {
|
||||
|
|
|
|||
|
|
@ -10,16 +10,6 @@ logger = logging.getLogger("django.contrib.gis")
|
|||
class MySQLGISSchemaEditor(DatabaseSchemaEditor):
|
||||
sql_add_spatial_index = "CREATE SPATIAL INDEX %(index)s ON %(table)s(%(column)s)"
|
||||
|
||||
def skip_default(self, field):
|
||||
# Geometry fields are stored as BLOB/TEXT, for which MySQL < 8.0.13
|
||||
# doesn't support defaults.
|
||||
if (
|
||||
isinstance(field, GeometryField)
|
||||
and not self._supports_limited_data_type_defaults
|
||||
):
|
||||
return True
|
||||
return super().skip_default(field)
|
||||
|
||||
def quote_value(self, value):
|
||||
if isinstance(value, self.connection.ops.Adapter):
|
||||
return super().quote_value(str(value))
|
||||
|
|
|
|||
|
|
@ -204,7 +204,7 @@ class PostGISOperations(BaseSpatialOperations, DatabaseOperations):
|
|||
raise ImproperlyConfigured(
|
||||
'Cannot determine PostGIS version for database "%s" '
|
||||
'using command "SELECT postgis_lib_version()". '
|
||||
"GeoDjango requires at least PostGIS version 3.1. "
|
||||
"GeoDjango requires at least PostGIS version 3.2. "
|
||||
"Was the database created from a spatial database "
|
||||
"template?" % self.connection.settings_dict["NAME"]
|
||||
)
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ class SpatiaLiteOperations(BaseSpatialOperations, DatabaseOperations):
|
|||
"ForcePolygonCW": "ST_ForceLHR",
|
||||
"FromWKB": "ST_GeomFromWKB",
|
||||
"FromWKT": "ST_GeomFromText",
|
||||
"IsEmpty": "ST_IsEmpty",
|
||||
"Length": "ST_Length",
|
||||
"LineLocatePoint": "ST_Line_Locate_Point",
|
||||
"NumPoints": "ST_NPoints",
|
||||
|
|
@ -84,7 +85,7 @@ class SpatiaLiteOperations(BaseSpatialOperations, DatabaseOperations):
|
|||
|
||||
@cached_property
|
||||
def unsupported_functions(self):
|
||||
unsupported = {"GeometryDistance", "IsEmpty", "MemSize", "Rotate"}
|
||||
unsupported = {"GeometryDistance", "MemSize", "Rotate"}
|
||||
if not self.geom_lib_version():
|
||||
unsupported |= {"Azimuth", "GeoHash", "MakeValid"}
|
||||
if self.spatial_version < (5, 1):
|
||||
|
|
|
|||
|
|
@ -451,6 +451,10 @@ class IsEmpty(GeoFuncMixin, Transform):
|
|||
lookup_name = "isempty"
|
||||
output_field = BooleanField()
|
||||
|
||||
def as_sqlite(self, compiler, connection, **extra_context):
|
||||
sql, params = super().as_sql(compiler, connection, **extra_context)
|
||||
return "NULLIF(%s, -1)" % sql, params
|
||||
|
||||
|
||||
@BaseSpatialField.register_lookup
|
||||
class IsValid(OracleToleranceMixin, GeoFuncMixin, Transform):
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ class GISLookup(Lookup):
|
|||
band_lhs = None
|
||||
|
||||
def __init__(self, lhs, rhs):
|
||||
rhs, *self.rhs_params = rhs if isinstance(rhs, (list, tuple)) else [rhs]
|
||||
rhs, *self.rhs_params = rhs if isinstance(rhs, (list, tuple)) else (rhs,)
|
||||
super().__init__(lhs, rhs)
|
||||
self.template_params = {}
|
||||
self.process_rhs_params()
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@ import json
|
|||
|
||||
from django.contrib.postgres import lookups
|
||||
from django.contrib.postgres.forms import SimpleArrayField
|
||||
from django.contrib.postgres.utils import (
|
||||
CheckPostgresInstalledMixin,
|
||||
prefix_validation_error,
|
||||
)
|
||||
from django.contrib.postgres.validators import ArrayMaxLengthValidator
|
||||
from django.core import checks, exceptions
|
||||
from django.db.models import Field, Func, IntegerField, Transform, Value
|
||||
|
|
@ -9,7 +13,6 @@ from django.db.models.fields.mixins import CheckFieldDefaultMixin
|
|||
from django.db.models.lookups import Exact, In
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from ..utils import CheckPostgresInstalledMixin, prefix_validation_error
|
||||
from .utils import AttributeSetter
|
||||
|
||||
__all__ = ["ArrayField"]
|
||||
|
|
@ -132,6 +135,11 @@ class ArrayField(CheckPostgresInstalledMixin, CheckFieldDefaultMixin, Field):
|
|||
]
|
||||
return value
|
||||
|
||||
def get_db_prep_save(self, value, connection):
|
||||
if isinstance(value, (list, tuple)):
|
||||
return [self.base_field.get_db_prep_save(i, connection) for i in value]
|
||||
return value
|
||||
|
||||
def deconstruct(self):
|
||||
name, path, args, kwargs = super().deconstruct()
|
||||
if path == "django.contrib.postgres.fields.array.ArrayField":
|
||||
|
|
|
|||
|
|
@ -2,13 +2,12 @@ import json
|
|||
|
||||
from django.contrib.postgres import forms, lookups
|
||||
from django.contrib.postgres.fields.array import ArrayField
|
||||
from django.contrib.postgres.utils import CheckPostgresInstalledMixin
|
||||
from django.core import exceptions
|
||||
from django.db.models import Field, TextField, Transform
|
||||
from django.db.models.fields.mixins import CheckFieldDefaultMixin
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from ..utils import CheckPostgresInstalledMixin
|
||||
|
||||
__all__ = ["HStoreField"]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import datetime
|
|||
import json
|
||||
|
||||
from django.contrib.postgres import forms, lookups
|
||||
from django.contrib.postgres.utils import CheckPostgresInstalledMixin
|
||||
from django.db import models
|
||||
from django.db.backends.postgresql.psycopg_any import (
|
||||
DateRange,
|
||||
|
|
@ -12,7 +13,6 @@ from django.db.backends.postgresql.psycopg_any import (
|
|||
from django.db.models.functions import Cast
|
||||
from django.db.models.lookups import PostgresOperatorLookup
|
||||
|
||||
from ..utils import CheckPostgresInstalledMixin
|
||||
from .utils import AttributeSetter
|
||||
|
||||
__all__ = [
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import copy
|
|||
from itertools import chain
|
||||
|
||||
from django import forms
|
||||
from django.contrib.postgres.utils import prefix_validation_error
|
||||
from django.contrib.postgres.validators import (
|
||||
ArrayMaxLengthValidator,
|
||||
ArrayMinLengthValidator,
|
||||
|
|
@ -9,8 +10,6 @@ from django.contrib.postgres.validators import (
|
|||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from ..utils import prefix_validation_error
|
||||
|
||||
|
||||
class SimpleArrayField(forms.CharField):
|
||||
default_error_messages = {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
from django.conf import settings
|
||||
|
||||
from .. import Error, Tags, register
|
||||
from django.core.checks import Error, Tags, register
|
||||
|
||||
|
||||
@register(Tags.compatibility)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
from django.conf import settings
|
||||
from django.core.checks import Error, Tags, Warning, register
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
from .. import Error, Tags, Warning, register
|
||||
|
||||
CROSS_ORIGIN_OPENER_POLICY_VALUES = {
|
||||
"same-origin",
|
||||
"same-origin-allow-popups",
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
import inspect
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from .. import Error, Tags, Warning, register
|
||||
from django.core.checks import Error, Tags, Warning, register
|
||||
|
||||
W003 = Warning(
|
||||
"You don't appear to be using Django's built-in "
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
from django.conf import settings
|
||||
|
||||
from .. import Tags, Warning, register
|
||||
from django.core.checks import Tags, Warning, register
|
||||
|
||||
|
||||
def add_session_cookie_message(message):
|
||||
|
|
|
|||
|
|
@ -132,6 +132,12 @@ class FieldError(Exception):
|
|||
pass
|
||||
|
||||
|
||||
class FieldFetchBlocked(FieldError):
|
||||
"""On-demand fetching of a model field blocked."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
NON_FIELD_ERRORS = "__all__"
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import re
|
|||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.db import DEFAULT_DB_ALIAS, connections
|
||||
from django.db.models.constants import LOOKUP_SEP
|
||||
from django.db.models.deletion import DatabaseOnDelete
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
|
|
@ -163,7 +164,9 @@ class Command(BaseCommand):
|
|||
extra_params["unique"] = True
|
||||
|
||||
if is_relation:
|
||||
ref_db_column, ref_db_table = relations[column_name]
|
||||
ref_db_column, ref_db_table, db_on_delete = relations[
|
||||
column_name
|
||||
]
|
||||
if extra_params.pop("unique", False) or extra_params.get(
|
||||
"primary_key"
|
||||
):
|
||||
|
|
@ -191,6 +194,8 @@ class Command(BaseCommand):
|
|||
model_name.lower(),
|
||||
att_name,
|
||||
)
|
||||
if db_on_delete and isinstance(db_on_delete, DatabaseOnDelete):
|
||||
extra_params["on_delete"] = f"models.{db_on_delete}"
|
||||
used_relations.add(rel_to)
|
||||
else:
|
||||
# Calling `get_field_type` to get the field type string
|
||||
|
|
@ -227,8 +232,12 @@ class Command(BaseCommand):
|
|||
"" if "." in field_type else "models.",
|
||||
field_type,
|
||||
)
|
||||
on_delete_qualname = extra_params.pop("on_delete", None)
|
||||
if field_type.startswith(("ForeignKey(", "OneToOneField(")):
|
||||
field_desc += ", models.DO_NOTHING"
|
||||
if on_delete_qualname:
|
||||
field_desc += f", {on_delete_qualname}"
|
||||
else:
|
||||
field_desc += ", models.DO_NOTHING"
|
||||
|
||||
# Add comment.
|
||||
if connection.features.supports_comments and row.comment:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
from django.core.checks.security.base import SECRET_KEY_INSECURE_PREFIX
|
||||
from django.core.management.templates import TemplateCommand
|
||||
|
||||
from ..utils import get_random_secret_key
|
||||
from django.core.management.utils import get_random_secret_key
|
||||
|
||||
|
||||
class Command(TemplateCommand):
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ from django.core.exceptions import ImproperlyConfigured
|
|||
from django.core.handlers.wsgi import LimitedStream
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
from django.db import connections
|
||||
from django.utils.log import log_message
|
||||
from django.utils.module_loading import import_string
|
||||
|
||||
__all__ = ("WSGIServer", "WSGIRequestHandler")
|
||||
|
|
@ -182,35 +183,27 @@ class WSGIRequestHandler(simple_server.WSGIRequestHandler):
|
|||
return self.client_address[0]
|
||||
|
||||
def log_message(self, format, *args):
|
||||
extra = {
|
||||
"request": self.request,
|
||||
"server_time": self.log_date_time_string(),
|
||||
}
|
||||
if args[1][0] == "4":
|
||||
if args[1][0] == "4" and args[0].startswith("\x16\x03"):
|
||||
# 0x16 = Handshake, 0x03 = SSL 3.0 or TLS 1.x
|
||||
if args[0].startswith("\x16\x03"):
|
||||
extra["status_code"] = 500
|
||||
logger.error(
|
||||
"You're accessing the development server over HTTPS, but "
|
||||
"it only supports HTTP.",
|
||||
extra=extra,
|
||||
)
|
||||
return
|
||||
|
||||
if args[1].isdigit() and len(args[1]) == 3:
|
||||
format = (
|
||||
"You're accessing the development server over HTTPS, but it only "
|
||||
"supports HTTP."
|
||||
)
|
||||
status_code = 500
|
||||
args = ()
|
||||
elif args[1].isdigit() and len(args[1]) == 3:
|
||||
status_code = int(args[1])
|
||||
extra["status_code"] = status_code
|
||||
|
||||
if status_code >= 500:
|
||||
level = logger.error
|
||||
elif status_code >= 400:
|
||||
level = logger.warning
|
||||
else:
|
||||
level = logger.info
|
||||
else:
|
||||
level = logger.info
|
||||
status_code = None
|
||||
|
||||
level(format, *args, extra=extra)
|
||||
log_message(
|
||||
logger,
|
||||
format,
|
||||
*args,
|
||||
request=self.request,
|
||||
status_code=status_code,
|
||||
server_time=self.log_date_time_string(),
|
||||
)
|
||||
|
||||
def get_environ(self):
|
||||
# Strip all headers with underscores in the name before constructing
|
||||
|
|
|
|||
|
|
@ -390,6 +390,9 @@ class BaseDatabaseFeatures:
|
|||
# subqueries?
|
||||
supports_tuple_comparison_against_subquery = True
|
||||
|
||||
# Does the backend support DEFAULT as delete option?
|
||||
supports_on_delete_db_default = True
|
||||
|
||||
# Collation names for use by the Django test suite.
|
||||
test_collations = {
|
||||
"ci": None, # Case-insensitive.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
from collections import namedtuple
|
||||
|
||||
from django.db.models import DB_CASCADE, DB_SET_DEFAULT, DB_SET_NULL, DO_NOTHING
|
||||
|
||||
# Structure returned by DatabaseIntrospection.get_table_list()
|
||||
TableInfo = namedtuple("TableInfo", ["name", "type"])
|
||||
|
||||
|
|
@ -15,6 +17,13 @@ class BaseDatabaseIntrospection:
|
|||
"""Encapsulate backend-specific introspection utilities."""
|
||||
|
||||
data_types_reverse = {}
|
||||
on_delete_types = {
|
||||
"CASCADE": DB_CASCADE,
|
||||
"NO ACTION": DO_NOTHING,
|
||||
"SET DEFAULT": DB_SET_DEFAULT,
|
||||
"SET NULL": DB_SET_NULL,
|
||||
# DB_RESTRICT - "RESTRICT" is not supported.
|
||||
}
|
||||
|
||||
def __init__(self, connection):
|
||||
self.connection = connection
|
||||
|
|
@ -169,8 +178,11 @@ class BaseDatabaseIntrospection:
|
|||
|
||||
def get_relations(self, cursor, table_name):
|
||||
"""
|
||||
Return a dictionary of {field_name: (field_name_other_table,
|
||||
other_table)} representing all foreign keys in the given table.
|
||||
Return a dictionary of
|
||||
{
|
||||
field_name: (field_name_other_table, other_table, db_on_delete)
|
||||
}
|
||||
representing all foreign keys in the given table.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"subclasses of BaseDatabaseIntrospection may require a "
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ from django.conf import settings
|
|||
from django.db import NotSupportedError, transaction
|
||||
from django.db.models.expressions import Col
|
||||
from django.utils import timezone
|
||||
from django.utils.duration import duration_microseconds
|
||||
from django.utils.encoding import force_str
|
||||
|
||||
|
||||
|
|
@ -253,6 +254,16 @@ class BaseDatabaseOperations:
|
|||
if sql
|
||||
)
|
||||
|
||||
def fk_on_delete_sql(self, operation):
|
||||
"""
|
||||
Return the SQL to make an ON DELETE statement.
|
||||
"""
|
||||
if operation in ["CASCADE", "SET NULL", "SET DEFAULT"]:
|
||||
return f" ON DELETE {operation}"
|
||||
if operation == "":
|
||||
return ""
|
||||
raise NotImplementedError(f"ON DELETE {operation} is not supported.")
|
||||
|
||||
def bulk_insert_sql(self, fields, placeholder_rows):
|
||||
placeholder_rows_sql = (", ".join(row) for row in placeholder_rows)
|
||||
values_sql = ", ".join([f"({sql})" for sql in placeholder_rows_sql])
|
||||
|
|
@ -564,6 +575,16 @@ class BaseDatabaseOperations:
|
|||
return None
|
||||
return str(value)
|
||||
|
||||
def adapt_durationfield_value(self, value):
|
||||
"""
|
||||
Transform a timedelta value into an object compatible with what is
|
||||
expected by the backend driver for duration columns (by default,
|
||||
an integer of microseconds).
|
||||
"""
|
||||
if value is None:
|
||||
return None
|
||||
return duration_microseconds(value)
|
||||
|
||||
def adapt_timefield_value(self, value):
|
||||
"""
|
||||
Transform a time value to an object compatible with what is expected
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ class BaseDatabaseSchemaEditor:
|
|||
|
||||
sql_create_fk = (
|
||||
"ALTER TABLE %(table)s ADD CONSTRAINT %(name)s FOREIGN KEY (%(column)s) "
|
||||
"REFERENCES %(to_table)s (%(to_column)s)%(deferrable)s"
|
||||
"REFERENCES %(to_table)s (%(to_column)s)%(on_delete_db)s%(deferrable)s"
|
||||
)
|
||||
sql_create_inline_fk = None
|
||||
sql_create_column_inline_fk = None
|
||||
|
|
@ -241,6 +241,7 @@ class BaseDatabaseSchemaEditor:
|
|||
definition += " " + self.sql_create_inline_fk % {
|
||||
"to_table": self.quote_name(to_table),
|
||||
"to_column": self.quote_name(to_column),
|
||||
"on_delete_db": self._create_on_delete_sql(model, field),
|
||||
}
|
||||
elif self.connection.features.supports_foreign_keys:
|
||||
self.deferred_sql.append(
|
||||
|
|
@ -759,6 +760,7 @@ class BaseDatabaseSchemaEditor:
|
|||
"to_table": self.quote_name(to_table),
|
||||
"to_column": self.quote_name(to_column),
|
||||
"deferrable": self.connection.ops.deferrable_sql(),
|
||||
"on_delete_db": self._create_on_delete_sql(model, field),
|
||||
}
|
||||
# Otherwise, add FK constraints later.
|
||||
else:
|
||||
|
|
@ -1628,6 +1630,13 @@ class BaseDatabaseSchemaEditor:
|
|||
new_name=self.quote_name(new_name),
|
||||
)
|
||||
|
||||
def _create_on_delete_sql(self, model, field):
|
||||
remote_field = field.remote_field
|
||||
try:
|
||||
return remote_field.on_delete.on_delete_sql(self)
|
||||
except AttributeError:
|
||||
return ""
|
||||
|
||||
def _index_columns(self, table, columns, col_suffixes, opclasses):
|
||||
return Columns(table, columns, self.quote_name, col_suffixes=col_suffixes)
|
||||
|
||||
|
|
@ -1660,7 +1669,9 @@ class BaseDatabaseSchemaEditor:
|
|||
return output
|
||||
|
||||
def _field_should_be_altered(self, old_field, new_field, ignore=None):
|
||||
if not old_field.concrete and not new_field.concrete:
|
||||
if (not (old_field.concrete or old_field.many_to_many)) and (
|
||||
not (new_field.concrete or new_field.many_to_many)
|
||||
):
|
||||
return False
|
||||
ignore = ignore or set()
|
||||
_, old_path, old_args, old_kwargs = old_field.deconstruct()
|
||||
|
|
@ -1692,8 +1703,10 @@ class BaseDatabaseSchemaEditor:
|
|||
):
|
||||
old_kwargs.pop("db_default")
|
||||
new_kwargs.pop("db_default")
|
||||
return self.quote_name(old_field.column) != self.quote_name(
|
||||
new_field.column
|
||||
return (
|
||||
old_field.concrete
|
||||
and new_field.concrete
|
||||
and (self.quote_name(old_field.column) != self.quote_name(new_field.column))
|
||||
) or (old_path, old_args, old_kwargs) != (new_path, new_args, new_kwargs)
|
||||
|
||||
def _field_should_be_indexed(self, model, field):
|
||||
|
|
@ -1736,6 +1749,7 @@ class BaseDatabaseSchemaEditor:
|
|||
to_table=to_table,
|
||||
to_column=to_column,
|
||||
deferrable=deferrable,
|
||||
on_delete_db=self._create_on_delete_sql(model, field),
|
||||
)
|
||||
|
||||
def _fk_constraint_name(self, model, field, suffix):
|
||||
|
|
|
|||
|
|
@ -144,11 +144,8 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
|||
_data_types["UUIDField"] = "uuid"
|
||||
return _data_types
|
||||
|
||||
# For these data types:
|
||||
# - MySQL < 8.0.13 doesn't accept default values and implicitly treats them
|
||||
# as nullable
|
||||
# - all versions of MySQL and MariaDB don't support full width database
|
||||
# indexes
|
||||
# For these data types MySQL and MariaDB don't support full width database
|
||||
# indexes.
|
||||
_limited_data_types = (
|
||||
"tinyblob",
|
||||
"blob",
|
||||
|
|
@ -337,6 +334,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
|||
for column_name, (
|
||||
referenced_column_name,
|
||||
referenced_table_name,
|
||||
_,
|
||||
) in relations.items():
|
||||
cursor.execute(
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
|
|||
SET V_I = P_I;
|
||||
END;
|
||||
"""
|
||||
supports_on_delete_db_default = False
|
||||
# Neither MySQL nor MariaDB support partial indexes.
|
||||
supports_partial_indexes = False
|
||||
# COLLATE must be wrapped in parentheses because MySQL treats COLLATE as an
|
||||
|
|
@ -65,7 +66,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
|
|||
if self.connection.mysql_is_mariadb:
|
||||
return (10, 6)
|
||||
else:
|
||||
return (8, 0, 11)
|
||||
return (8, 4)
|
||||
|
||||
@cached_property
|
||||
def test_collations(self):
|
||||
|
|
@ -103,24 +104,6 @@ class DatabaseFeatures(BaseDatabaseFeatures):
|
|||
"update.tests.AdvancedTests.test_update_ordered_by_m2m_annotation_desc",
|
||||
},
|
||||
}
|
||||
if not self.supports_explain_analyze:
|
||||
skips.update(
|
||||
{
|
||||
"MariaDB and MySQL >= 8.0.18 specific.": {
|
||||
"queries.test_explain.ExplainTests.test_mysql_analyze",
|
||||
},
|
||||
}
|
||||
)
|
||||
if self.connection.mysql_version < (8, 0, 31):
|
||||
skips.update(
|
||||
{
|
||||
"Nesting of UNIONs at the right-hand side is not supported on "
|
||||
"MySQL < 8.0.31": {
|
||||
"queries.test_qs_combinators.QuerySetSetOperationTests."
|
||||
"test_union_nested"
|
||||
},
|
||||
}
|
||||
)
|
||||
if not self.connection.mysql_is_mariadb:
|
||||
skips.update(
|
||||
{
|
||||
|
|
@ -185,44 +168,16 @@ class DatabaseFeatures(BaseDatabaseFeatures):
|
|||
def is_sql_auto_is_null_enabled(self):
|
||||
return self.connection.mysql_server_data["sql_auto_is_null"]
|
||||
|
||||
@cached_property
|
||||
def supports_column_check_constraints(self):
|
||||
if self.connection.mysql_is_mariadb:
|
||||
return True
|
||||
return self.connection.mysql_version >= (8, 0, 16)
|
||||
|
||||
supports_table_check_constraints = property(
|
||||
operator.attrgetter("supports_column_check_constraints")
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def can_introspect_check_constraints(self):
|
||||
if self.connection.mysql_is_mariadb:
|
||||
return True
|
||||
return self.connection.mysql_version >= (8, 0, 16)
|
||||
|
||||
@cached_property
|
||||
def has_select_for_update_of(self):
|
||||
return not self.connection.mysql_is_mariadb
|
||||
|
||||
@cached_property
|
||||
def supports_explain_analyze(self):
|
||||
return self.connection.mysql_is_mariadb or self.connection.mysql_version >= (
|
||||
8,
|
||||
0,
|
||||
18,
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def supported_explain_formats(self):
|
||||
# Alias MySQL's TRADITIONAL to TEXT for consistency with other
|
||||
# backends.
|
||||
formats = {"JSON", "TEXT", "TRADITIONAL"}
|
||||
if not self.connection.mysql_is_mariadb and self.connection.mysql_version >= (
|
||||
8,
|
||||
0,
|
||||
16,
|
||||
):
|
||||
if not self.connection.mysql_is_mariadb:
|
||||
formats.add("TREE")
|
||||
return formats
|
||||
|
||||
|
|
@ -261,24 +216,8 @@ class DatabaseFeatures(BaseDatabaseFeatures):
|
|||
return (
|
||||
not self.connection.mysql_is_mariadb
|
||||
and self._mysql_storage_engine != "MyISAM"
|
||||
and self.connection.mysql_version >= (8, 0, 13)
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def supports_select_intersection(self):
|
||||
is_mariadb = self.connection.mysql_is_mariadb
|
||||
return is_mariadb or self.connection.mysql_version >= (8, 0, 31)
|
||||
|
||||
supports_select_difference = property(
|
||||
operator.attrgetter("supports_select_intersection")
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def supports_expression_defaults(self):
|
||||
if self.connection.mysql_is_mariadb:
|
||||
return True
|
||||
return self.connection.mysql_version >= (8, 0, 13)
|
||||
|
||||
@cached_property
|
||||
def has_native_uuid_field(self):
|
||||
is_mariadb = self.connection.mysql_is_mariadb
|
||||
|
|
|
|||
|
|
@ -196,24 +196,36 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
|
|||
|
||||
def get_relations(self, cursor, table_name):
|
||||
"""
|
||||
Return a dictionary of {field_name: (field_name_other_table,
|
||||
other_table)} representing all foreign keys in the given table.
|
||||
Return a dictionary of
|
||||
{
|
||||
field_name: (field_name_other_table, other_table, db_on_delete)
|
||||
}
|
||||
representing all foreign keys in the given table.
|
||||
"""
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT column_name, referenced_column_name, referenced_table_name
|
||||
FROM information_schema.key_column_usage
|
||||
WHERE table_name = %s
|
||||
AND table_schema = DATABASE()
|
||||
AND referenced_table_schema = DATABASE()
|
||||
AND referenced_table_name IS NOT NULL
|
||||
AND referenced_column_name IS NOT NULL
|
||||
SELECT
|
||||
kcu.column_name,
|
||||
kcu.referenced_column_name,
|
||||
kcu.referenced_table_name,
|
||||
rc.delete_rule
|
||||
FROM
|
||||
information_schema.key_column_usage kcu
|
||||
JOIN
|
||||
information_schema.referential_constraints rc
|
||||
ON rc.constraint_name = kcu.constraint_name
|
||||
AND rc.constraint_schema = kcu.constraint_schema
|
||||
WHERE kcu.table_name = %s
|
||||
AND kcu.table_schema = DATABASE()
|
||||
AND kcu.referenced_table_schema = DATABASE()
|
||||
AND kcu.referenced_table_name IS NOT NULL
|
||||
AND kcu.referenced_column_name IS NOT NULL
|
||||
""",
|
||||
[table_name],
|
||||
)
|
||||
return {
|
||||
field_name: (other_field, other_table)
|
||||
for field_name, other_field, other_table in cursor.fetchall()
|
||||
field_name: (other_field, other_table, self.on_delete_types.get(on_delete))
|
||||
for field_name, other_field, other_table, on_delete in cursor.fetchall()
|
||||
}
|
||||
|
||||
def get_storage_engine(self, cursor, table_name):
|
||||
|
|
|
|||
|
|
@ -349,7 +349,7 @@ class DatabaseOperations(BaseDatabaseOperations):
|
|||
format = "TREE"
|
||||
analyze = options.pop("analyze", False)
|
||||
prefix = super().explain_query_prefix(format, **options)
|
||||
if analyze and self.connection.features.supports_explain_analyze:
|
||||
if analyze:
|
||||
# MariaDB uses ANALYZE instead of EXPLAIN ANALYZE.
|
||||
prefix = (
|
||||
"ANALYZE" if self.connection.mysql_is_mariadb else prefix + " ANALYZE"
|
||||
|
|
@ -407,15 +407,11 @@ class DatabaseOperations(BaseDatabaseOperations):
|
|||
def on_conflict_suffix_sql(self, fields, on_conflict, update_fields, unique_fields):
|
||||
if on_conflict == OnConflict.UPDATE:
|
||||
conflict_suffix_sql = "ON DUPLICATE KEY UPDATE %(fields)s"
|
||||
# The use of VALUES() is deprecated in MySQL 8.0.20+. Instead, use
|
||||
# aliases for the new row and its columns available in MySQL
|
||||
# 8.0.19+.
|
||||
# The use of VALUES() is not supported in MySQL. Instead, use
|
||||
# aliases for the new row and its columns.
|
||||
if not self.connection.mysql_is_mariadb:
|
||||
if self.connection.mysql_version >= (8, 0, 19):
|
||||
conflict_suffix_sql = f"AS new {conflict_suffix_sql}"
|
||||
field_sql = "%(field)s = new.%(field)s"
|
||||
else:
|
||||
field_sql = "%(field)s = VALUES(%(field)s)"
|
||||
conflict_suffix_sql = f"AS new {conflict_suffix_sql}"
|
||||
field_sql = "%(field)s = new.%(field)s"
|
||||
# Use VALUE() on MariaDB.
|
||||
else:
|
||||
field_sql = "%(field)s = VALUE(%(field)s)"
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
|
|||
sql_delete_unique = "ALTER TABLE %(table)s DROP INDEX %(name)s"
|
||||
sql_create_column_inline_fk = (
|
||||
", ADD CONSTRAINT %(name)s FOREIGN KEY (%(column)s) "
|
||||
"REFERENCES %(to_table)s(%(to_column)s)"
|
||||
"REFERENCES %(to_table)s(%(to_column)s)%(on_delete_db)s"
|
||||
)
|
||||
sql_delete_fk = "ALTER TABLE %(table)s DROP FOREIGN KEY %(name)s"
|
||||
|
||||
|
|
@ -65,13 +65,10 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
|
|||
default_is_empty = self.effective_default(field) in ("", b"")
|
||||
if default_is_empty and self._is_text_or_blob(field):
|
||||
return True
|
||||
if not self._supports_limited_data_type_defaults:
|
||||
return self._is_limited_data_type(field)
|
||||
return False
|
||||
|
||||
def skip_default_on_alter(self, field):
|
||||
default_is_empty = self.effective_default(field) in ("", b"")
|
||||
if default_is_empty and self._is_text_or_blob(field):
|
||||
if self.skip_default(field):
|
||||
return True
|
||||
if self._is_limited_data_type(field) and not self.connection.mysql_is_mariadb:
|
||||
# MySQL doesn't support defaults for BLOB and TEXT in the
|
||||
|
|
@ -79,19 +76,8 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
|
|||
return True
|
||||
return False
|
||||
|
||||
@property
|
||||
def _supports_limited_data_type_defaults(self):
|
||||
# MariaDB and MySQL >= 8.0.13 support defaults for BLOB and TEXT.
|
||||
if self.connection.mysql_is_mariadb:
|
||||
return True
|
||||
return self.connection.mysql_version >= (8, 0, 13)
|
||||
|
||||
def _column_default_sql(self, field):
|
||||
if (
|
||||
not self.connection.mysql_is_mariadb
|
||||
and self._supports_limited_data_type_defaults
|
||||
and self._is_limited_data_type(field)
|
||||
):
|
||||
if not self.connection.mysql_is_mariadb and self._is_limited_data_type(field):
|
||||
# MySQL supports defaults for BLOB and TEXT columns only if the
|
||||
# default value is written as an expression i.e. in parentheses.
|
||||
return "(%s)"
|
||||
|
|
|
|||
|
|
@ -433,7 +433,7 @@ class OracleParam:
|
|||
param = 0
|
||||
if hasattr(param, "bind_parameter"):
|
||||
self.force_bytes = param.bind_parameter(cursor)
|
||||
elif isinstance(param, (Database.Binary, datetime.timedelta)):
|
||||
elif isinstance(param, (bytes, datetime.timedelta)):
|
||||
self.force_bytes = param
|
||||
else:
|
||||
# To transmit to the database, we need Unicode if supported
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
|
|||
supports_json_field_contains = False
|
||||
supports_json_negative_indexing = False
|
||||
supports_collation_on_textfield = False
|
||||
supports_on_delete_db_default = False
|
||||
test_now_utc_template = "CURRENT_TIMESTAMP AT TIME ZONE 'UTC'"
|
||||
django_test_expected_failures = {
|
||||
# A bug in Django/oracledb with respect to string handling (#23843).
|
||||
|
|
|
|||
|
|
@ -254,13 +254,16 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
|
|||
|
||||
def get_relations(self, cursor, table_name):
|
||||
"""
|
||||
Return a dictionary of {field_name: (field_name_other_table,
|
||||
other_table)} representing all foreign keys in the given table.
|
||||
Return a dictionary of
|
||||
{
|
||||
field_name: (field_name_other_table, other_table, db_on_delete)
|
||||
}
|
||||
representing all foreign keys in the given table.
|
||||
"""
|
||||
table_name = table_name.upper()
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT ca.column_name, cb.table_name, cb.column_name
|
||||
SELECT ca.column_name, cb.table_name, cb.column_name, user_constraints.delete_rule
|
||||
FROM user_constraints, USER_CONS_COLUMNS ca, USER_CONS_COLUMNS cb
|
||||
WHERE user_constraints.table_name = %s AND
|
||||
user_constraints.constraint_name = ca.constraint_name AND
|
||||
|
|
@ -273,8 +276,14 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
|
|||
self.identifier_converter(field_name): (
|
||||
self.identifier_converter(rel_field_name),
|
||||
self.identifier_converter(rel_table_name),
|
||||
self.on_delete_types.get(on_delete),
|
||||
)
|
||||
for field_name, rel_table_name, rel_field_name in cursor.fetchall()
|
||||
for (
|
||||
field_name,
|
||||
rel_table_name,
|
||||
rel_field_name,
|
||||
on_delete,
|
||||
) in cursor.fetchall()
|
||||
}
|
||||
|
||||
def get_primary_key_columns(self, cursor, table_name):
|
||||
|
|
|
|||
|
|
@ -273,12 +273,12 @@ END;
|
|||
return value
|
||||
|
||||
def convert_datefield_value(self, value, expression, connection):
|
||||
if isinstance(value, Database.Timestamp):
|
||||
if isinstance(value, datetime.datetime):
|
||||
value = value.date()
|
||||
return value
|
||||
|
||||
def convert_timefield_value(self, value, expression, connection):
|
||||
if isinstance(value, Database.Timestamp):
|
||||
if isinstance(value, datetime.datetime):
|
||||
value = value.time()
|
||||
return value
|
||||
|
||||
|
|
@ -608,6 +608,9 @@ END;
|
|||
|
||||
return Oracle_datetime.from_datetime(value)
|
||||
|
||||
def adapt_durationfield_value(self, value):
|
||||
return value
|
||||
|
||||
def adapt_timefield_value(self, value):
|
||||
if value is None:
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -20,7 +20,8 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
|
|||
sql_alter_column_no_default_null = sql_alter_column_no_default
|
||||
|
||||
sql_create_column_inline_fk = (
|
||||
"CONSTRAINT %(name)s REFERENCES %(to_table)s(%(to_column)s)%(deferrable)s"
|
||||
"CONSTRAINT %(name)s REFERENCES %(to_table)s(%(to_column)s)%(on_delete_db)"
|
||||
"s%(deferrable)s"
|
||||
)
|
||||
sql_delete_table = "DROP TABLE %(table)s CASCADE CONSTRAINTS"
|
||||
sql_create_index = "CREATE INDEX %(name)s ON %(table)s (%(columns)s)%(extra)s"
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ class BoundVar:
|
|||
"BooleanField": int,
|
||||
"FloatField": Database.DB_TYPE_BINARY_DOUBLE,
|
||||
"DateTimeField": Database.DB_TYPE_TIMESTAMP,
|
||||
"DateField": Database.Date,
|
||||
"DateField": datetime.date,
|
||||
"DecimalField": decimal.Decimal,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
from django.db.models.sql.compiler import (
|
||||
from django.db.models.sql.compiler import ( # isort:skip
|
||||
SQLAggregateCompiler,
|
||||
SQLCompiler,
|
||||
SQLDeleteCompiler,
|
||||
SQLInsertCompiler as BaseSQLInsertCompiler,
|
||||
SQLUpdateCompiler,
|
||||
)
|
||||
from django.db.models.sql.compiler import SQLInsertCompiler as BaseSQLInsertCompiler
|
||||
from django.db.models.sql.compiler import SQLUpdateCompiler
|
||||
|
||||
__all__ = [
|
||||
"SQLAggregateCompiler",
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from django.utils.functional import cached_property
|
|||
|
||||
|
||||
class DatabaseFeatures(BaseDatabaseFeatures):
|
||||
minimum_database_version = (14,)
|
||||
minimum_database_version = (15,)
|
||||
allows_group_by_selected_pks = True
|
||||
can_return_columns_from_insert = True
|
||||
can_return_rows_from_bulk_insert = True
|
||||
|
|
@ -67,7 +67,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
|
|||
supports_update_conflicts_with_target = True
|
||||
supports_covering_indexes = True
|
||||
supports_stored_generated_columns = True
|
||||
supports_virtual_generated_columns = False
|
||||
supports_nulls_distinct_unique_constraints = True
|
||||
can_rename_index = True
|
||||
test_collations = {
|
||||
"deterministic": "C",
|
||||
|
|
@ -156,10 +156,6 @@ class DatabaseFeatures(BaseDatabaseFeatures):
|
|||
"PositiveSmallIntegerField": "SmallIntegerField",
|
||||
}
|
||||
|
||||
@cached_property
|
||||
def is_postgresql_15(self):
|
||||
return self.connection.pg_version >= 150000
|
||||
|
||||
@cached_property
|
||||
def is_postgresql_16(self):
|
||||
return self.connection.pg_version >= 160000
|
||||
|
|
@ -168,9 +164,12 @@ class DatabaseFeatures(BaseDatabaseFeatures):
|
|||
def is_postgresql_17(self):
|
||||
return self.connection.pg_version >= 170000
|
||||
|
||||
supports_unlimited_charfield = True
|
||||
supports_nulls_distinct_unique_constraints = property(
|
||||
operator.attrgetter("is_postgresql_15")
|
||||
)
|
||||
@cached_property
|
||||
def is_postgresql_18(self):
|
||||
return self.connection.pg_version >= 180000
|
||||
|
||||
supports_unlimited_charfield = True
|
||||
supports_any_value = property(operator.attrgetter("is_postgresql_16"))
|
||||
supports_virtual_generated_columns = property(
|
||||
operator.attrgetter("is_postgresql_18")
|
||||
)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from collections import namedtuple
|
|||
from django.db.backends.base.introspection import BaseDatabaseIntrospection
|
||||
from django.db.backends.base.introspection import FieldInfo as BaseFieldInfo
|
||||
from django.db.backends.base.introspection import TableInfo as BaseTableInfo
|
||||
from django.db.models import Index
|
||||
from django.db.models import DB_CASCADE, DB_SET_DEFAULT, DB_SET_NULL, DO_NOTHING, Index
|
||||
|
||||
FieldInfo = namedtuple("FieldInfo", [*BaseFieldInfo._fields, "is_autofield", "comment"])
|
||||
TableInfo = namedtuple("TableInfo", [*BaseTableInfo._fields, "comment"])
|
||||
|
|
@ -38,6 +38,14 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
|
|||
|
||||
ignored_tables = []
|
||||
|
||||
on_delete_types = {
|
||||
"a": DO_NOTHING,
|
||||
"c": DB_CASCADE,
|
||||
"d": DB_SET_DEFAULT,
|
||||
"n": DB_SET_NULL,
|
||||
# DB_RESTRICT - "r" is not supported.
|
||||
}
|
||||
|
||||
def get_field_type(self, data_type, description):
|
||||
field_type = super().get_field_type(data_type, description)
|
||||
if description.is_autofield or (
|
||||
|
|
@ -154,12 +162,15 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
|
|||
|
||||
def get_relations(self, cursor, table_name):
|
||||
"""
|
||||
Return a dictionary of {field_name: (field_name_other_table,
|
||||
other_table)} representing all foreign keys in the given table.
|
||||
Return a dictionary of
|
||||
{
|
||||
field_name: (field_name_other_table, other_table, db_on_delete)
|
||||
}
|
||||
representing all foreign keys in the given table.
|
||||
"""
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT a1.attname, c2.relname, a2.attname
|
||||
SELECT a1.attname, c2.relname, a2.attname, con.confdeltype
|
||||
FROM pg_constraint con
|
||||
LEFT JOIN pg_class c1 ON con.conrelid = c1.oid
|
||||
LEFT JOIN pg_class c2 ON con.confrelid = c2.oid
|
||||
|
|
@ -175,7 +186,10 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
|
|||
""",
|
||||
[table_name],
|
||||
)
|
||||
return {row[0]: (row[2], row[1]) for row in cursor.fetchall()}
|
||||
return {
|
||||
row[0]: (row[2], row[1], self.on_delete_types.get(row[3]))
|
||||
for row in cursor.fetchall()
|
||||
}
|
||||
|
||||
def get_constraints(self, cursor, table_name):
|
||||
"""
|
||||
|
|
@ -206,7 +220,9 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
|
|||
cl.reloptions
|
||||
FROM pg_constraint AS c
|
||||
JOIN pg_class AS cl ON c.conrelid = cl.oid
|
||||
WHERE cl.relname = %s AND pg_catalog.pg_table_is_visible(cl.oid)
|
||||
WHERE cl.relname = %s
|
||||
AND pg_catalog.pg_table_is_visible(cl.oid)
|
||||
AND c.contype != 'n'
|
||||
""",
|
||||
[table_name],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -330,6 +330,9 @@ class DatabaseOperations(BaseDatabaseOperations):
|
|||
def adapt_datetimefield_value(self, value):
|
||||
return value
|
||||
|
||||
def adapt_durationfield_value(self, value):
|
||||
return value
|
||||
|
||||
def adapt_timefield_value(self, value):
|
||||
return value
|
||||
|
||||
|
|
|
|||
|
|
@ -28,8 +28,8 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
|
|||
# Setting the constraint to IMMEDIATE to allow changing data in the same
|
||||
# transaction.
|
||||
sql_create_column_inline_fk = (
|
||||
"CONSTRAINT %(name)s REFERENCES %(to_table)s(%(to_column)s)%(deferrable)s"
|
||||
"; SET CONSTRAINTS %(namespace)s%(name)s IMMEDIATE"
|
||||
"CONSTRAINT %(name)s REFERENCES %(to_table)s(%(to_column)s)%(on_delete_db)s"
|
||||
"%(deferrable)s; SET CONSTRAINTS %(namespace)s%(name)s IMMEDIATE"
|
||||
)
|
||||
# Setting the constraint to IMMEDIATE runs any deferred checks to allow
|
||||
# dropping it in the same transaction.
|
||||
|
|
|
|||
|
|
@ -155,5 +155,3 @@ class DatabaseCreation(BaseDatabaseCreation):
|
|||
# connection.
|
||||
self.connection.connect()
|
||||
target_db.close()
|
||||
if os.environ.get("RUNNING_DJANGOS_TEST_SUITE") == "true":
|
||||
self.mark_expected_failures_and_skips()
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ from .base import Database
|
|||
|
||||
|
||||
class DatabaseFeatures(BaseDatabaseFeatures):
|
||||
minimum_database_version = (3, 31)
|
||||
minimum_database_version = (3, 37)
|
||||
test_db_allows_multiple_connections = False
|
||||
supports_unspecified_pk = True
|
||||
supports_timezones = False
|
||||
|
|
@ -26,8 +26,6 @@ class DatabaseFeatures(BaseDatabaseFeatures):
|
|||
time_cast_precision = 3
|
||||
can_release_savepoints = True
|
||||
has_case_insensitive_like = True
|
||||
# Is "ALTER TABLE ... DROP COLUMN" supported?
|
||||
can_alter_table_drop_column = Database.sqlite_version_info >= (3, 35, 5)
|
||||
supports_parentheses_in_compound = False
|
||||
can_defer_constraint_checks = True
|
||||
supports_over_clause = True
|
||||
|
|
@ -57,6 +55,9 @@ class DatabaseFeatures(BaseDatabaseFeatures):
|
|||
insert_test_table_with_defaults = 'INSERT INTO {} ("null") VALUES (1)'
|
||||
supports_default_keyword_in_insert = False
|
||||
supports_unlimited_charfield = True
|
||||
can_return_columns_from_insert = True
|
||||
can_return_rows_from_bulk_insert = True
|
||||
can_return_rows_from_update = True
|
||||
|
||||
@cached_property
|
||||
def django_test_skips(self):
|
||||
|
|
@ -146,8 +147,8 @@ class DatabaseFeatures(BaseDatabaseFeatures):
|
|||
"""
|
||||
SQLite has a variable limit per query. The limit can be changed using
|
||||
the SQLITE_MAX_VARIABLE_NUMBER compile-time option (which defaults to
|
||||
999 in versions < 3.32.0 or 32766 in newer versions) or lowered per
|
||||
connection at run-time with setlimit(SQLITE_LIMIT_VARIABLE_NUMBER, N).
|
||||
32766) or lowered per connection at run-time with
|
||||
setlimit(SQLITE_LIMIT_VARIABLE_NUMBER, N).
|
||||
"""
|
||||
return self.connection.connection.getlimit(sqlite3.SQLITE_LIMIT_VARIABLE_NUMBER)
|
||||
|
||||
|
|
@ -163,15 +164,3 @@ class DatabaseFeatures(BaseDatabaseFeatures):
|
|||
|
||||
can_introspect_json_field = property(operator.attrgetter("supports_json_field"))
|
||||
has_json_object_function = property(operator.attrgetter("supports_json_field"))
|
||||
|
||||
@cached_property
|
||||
def can_return_columns_from_insert(self):
|
||||
return Database.sqlite_version_info >= (3, 35)
|
||||
|
||||
can_return_rows_from_bulk_insert = property(
|
||||
operator.attrgetter("can_return_columns_from_insert")
|
||||
)
|
||||
|
||||
can_return_rows_from_update = property(
|
||||
operator.attrgetter("can_return_columns_from_insert")
|
||||
)
|
||||
|
|
|
|||
|
|
@ -153,20 +153,27 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
|
|||
|
||||
def get_relations(self, cursor, table_name):
|
||||
"""
|
||||
Return a dictionary of {column_name: (ref_column_name, ref_table_name)}
|
||||
Return a dictionary of
|
||||
{column_name: (ref_column_name, ref_table_name, db_on_delete)}
|
||||
representing all foreign keys in the given table.
|
||||
"""
|
||||
cursor.execute(
|
||||
"PRAGMA foreign_key_list(%s)" % self.connection.ops.quote_name(table_name)
|
||||
)
|
||||
return {
|
||||
column_name: (ref_column_name, ref_table_name)
|
||||
column_name: (
|
||||
ref_column_name,
|
||||
ref_table_name,
|
||||
self.on_delete_types.get(on_delete),
|
||||
)
|
||||
for (
|
||||
_,
|
||||
_,
|
||||
ref_table_name,
|
||||
column_name,
|
||||
ref_column_name,
|
||||
_,
|
||||
on_delete,
|
||||
*_,
|
||||
) in cursor.fetchall()
|
||||
}
|
||||
|
|
@ -342,8 +349,7 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
|
|||
"PRAGMA index_list(%s)" % self.connection.ops.quote_name(table_name)
|
||||
)
|
||||
for row in cursor.fetchall():
|
||||
# SQLite 3.8.9+ has 5 columns, however older versions only give 3
|
||||
# columns. Discard last 2 columns if there.
|
||||
# Discard last 2 columns.
|
||||
number, index, unique = row[:3]
|
||||
cursor.execute(
|
||||
"SELECT sql FROM sqlite_master WHERE type='index' AND name=%s",
|
||||
|
|
@ -408,7 +414,10 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
|
|||
"check": False,
|
||||
"index": False,
|
||||
}
|
||||
for index, (column_name, (ref_column_name, ref_table_name)) in relations
|
||||
for index, (
|
||||
column_name,
|
||||
(ref_column_name, ref_table_name, _),
|
||||
) in relations
|
||||
}
|
||||
)
|
||||
return constraints
|
||||
|
|
|
|||
|
|
@ -13,7 +13,8 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
|
|||
sql_delete_table = "DROP TABLE %(table)s"
|
||||
sql_create_fk = None
|
||||
sql_create_inline_fk = (
|
||||
"REFERENCES %(to_table)s (%(to_column)s) DEFERRABLE INITIALLY DEFERRED"
|
||||
"REFERENCES %(to_table)s (%(to_column)s)%(on_delete_db)s DEFERRABLE INITIALLY "
|
||||
"DEFERRED"
|
||||
)
|
||||
sql_create_column_inline_fk = sql_create_inline_fk
|
||||
sql_create_unique = "CREATE UNIQUE INDEX %(name)s ON %(table)s (%(columns)s)"
|
||||
|
|
@ -144,7 +145,7 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
|
|||
# Choose a default and insert it into the copy map
|
||||
if (
|
||||
not create_field.has_db_default()
|
||||
and not (create_field.many_to_many or create_field.generated)
|
||||
and not create_field.generated
|
||||
and create_field.concrete
|
||||
):
|
||||
mapping[create_field.column] = self.prepare_default(
|
||||
|
|
@ -338,10 +339,9 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
|
|||
self.delete_model(field.remote_field.through)
|
||||
# For explicit "through" M2M fields, do nothing
|
||||
elif (
|
||||
self.connection.features.can_alter_table_drop_column
|
||||
# Primary keys, unique fields, indexed fields, and foreign keys are
|
||||
# not supported in ALTER TABLE DROP COLUMN.
|
||||
and not field.primary_key
|
||||
not field.primary_key
|
||||
and not field.unique
|
||||
and not field.db_index
|
||||
and not (field.remote_field and field.db_constraint)
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ from django.conf import SettingsReference
|
|||
from django.db import models
|
||||
from django.db.migrations.operations.base import Operation
|
||||
from django.db.migrations.utils import COMPILED_REGEX_TYPE, RegexObject
|
||||
from django.db.models.deletion import DatabaseOnDelete
|
||||
from django.utils.functional import LazyObject, Promise
|
||||
from django.utils.version import get_docs_version
|
||||
|
||||
|
|
@ -71,6 +72,12 @@ class ChoicesSerializer(BaseSerializer):
|
|||
return serializer_factory(self.value.value).serialize()
|
||||
|
||||
|
||||
class DatabaseOnDeleteSerializer(BaseSerializer):
|
||||
def serialize(self):
|
||||
path = self.value.__class__.__module__
|
||||
return f"{path}.{self.value.__name__}", {f"import {path}"}
|
||||
|
||||
|
||||
class DateTimeSerializer(BaseSerializer):
|
||||
"""For datetime.*, except datetime.datetime."""
|
||||
|
||||
|
|
@ -363,6 +370,7 @@ class Serializer:
|
|||
pathlib.PurePath: PathSerializer,
|
||||
os.PathLike: PathLikeSerializer,
|
||||
zoneinfo.ZoneInfo: ZoneInfoSerializer,
|
||||
DatabaseOnDelete: DatabaseOnDeleteSerializer,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@ from django.db.models.constraints import * # NOQA
|
|||
from django.db.models.constraints import __all__ as constraints_all
|
||||
from django.db.models.deletion import (
|
||||
CASCADE,
|
||||
DB_CASCADE,
|
||||
DB_SET_DEFAULT,
|
||||
DB_SET_NULL,
|
||||
DO_NOTHING,
|
||||
PROTECT,
|
||||
RESTRICT,
|
||||
|
|
@ -36,12 +39,13 @@ from django.db.models.expressions import (
|
|||
WindowFrame,
|
||||
WindowFrameExclusion,
|
||||
)
|
||||
from django.db.models.fetch_modes import FETCH_ONE, FETCH_PEERS, RAISE
|
||||
from django.db.models.fields import * # NOQA
|
||||
from django.db.models.fields import __all__ as fields_all
|
||||
from django.db.models.fields.composite import CompositePrimaryKey
|
||||
from django.db.models.fields.files import FileField, ImageField
|
||||
from django.db.models.fields.generated import GeneratedField
|
||||
from django.db.models.fields.json import JSONField
|
||||
from django.db.models.fields.json import JSONField, JSONNull
|
||||
from django.db.models.fields.proxy import OrderWrt
|
||||
from django.db.models.indexes import * # NOQA
|
||||
from django.db.models.indexes import __all__ as indexes_all
|
||||
|
|
@ -74,6 +78,9 @@ __all__ += [
|
|||
"ObjectDoesNotExist",
|
||||
"signals",
|
||||
"CASCADE",
|
||||
"DB_CASCADE",
|
||||
"DB_SET_DEFAULT",
|
||||
"DB_SET_NULL",
|
||||
"DO_NOTHING",
|
||||
"PROTECT",
|
||||
"RESTRICT",
|
||||
|
|
@ -90,6 +97,7 @@ __all__ += [
|
|||
"ExpressionWrapper",
|
||||
"F",
|
||||
"Func",
|
||||
"JSONNull",
|
||||
"OrderBy",
|
||||
"OuterRef",
|
||||
"RowRange",
|
||||
|
|
@ -105,6 +113,9 @@ __all__ += [
|
|||
"GeneratedField",
|
||||
"JSONField",
|
||||
"OrderWrt",
|
||||
"FETCH_ONE",
|
||||
"FETCH_PEERS",
|
||||
"RAISE",
|
||||
"Lookup",
|
||||
"Transform",
|
||||
"Manager",
|
||||
|
|
|
|||
|
|
@ -30,8 +30,9 @@ from django.db import (
|
|||
)
|
||||
from django.db.models import NOT_PROVIDED, ExpressionWrapper, IntegerField, Max, Value
|
||||
from django.db.models.constants import LOOKUP_SEP
|
||||
from django.db.models.deletion import CASCADE, Collector
|
||||
from django.db.models.deletion import CASCADE, DO_NOTHING, Collector, DatabaseOnDelete
|
||||
from django.db.models.expressions import DatabaseDefault
|
||||
from django.db.models.fetch_modes import FETCH_ONE
|
||||
from django.db.models.fields.composite import CompositePrimaryKey
|
||||
from django.db.models.fields.related import (
|
||||
ForeignObjectRel,
|
||||
|
|
@ -466,6 +467,14 @@ class ModelStateFieldsCacheDescriptor:
|
|||
return res
|
||||
|
||||
|
||||
class ModelStateFetchModeDescriptor:
|
||||
def __get__(self, instance, cls=None):
|
||||
if instance is None:
|
||||
return self
|
||||
res = instance.fetch_mode = FETCH_ONE
|
||||
return res
|
||||
|
||||
|
||||
class ModelState:
|
||||
"""Store model instance state."""
|
||||
|
||||
|
|
@ -476,6 +485,14 @@ class ModelState:
|
|||
# on the actual save.
|
||||
adding = True
|
||||
fields_cache = ModelStateFieldsCacheDescriptor()
|
||||
fetch_mode = ModelStateFetchModeDescriptor()
|
||||
peers = ()
|
||||
|
||||
def __getstate__(self):
|
||||
state = self.__dict__.copy()
|
||||
# Weak references can't be pickled.
|
||||
state.pop("peers", None)
|
||||
return state
|
||||
|
||||
|
||||
class Model(AltersData, metaclass=ModelBase):
|
||||
|
|
@ -595,7 +612,7 @@ class Model(AltersData, metaclass=ModelBase):
|
|||
post_init.send(sender=cls, instance=self)
|
||||
|
||||
@classmethod
|
||||
def from_db(cls, db, field_names, values):
|
||||
def from_db(cls, db, field_names, values, *, fetch_mode=None):
|
||||
if len(values) != len(cls._meta.concrete_fields):
|
||||
values_iter = iter(values)
|
||||
values = [
|
||||
|
|
@ -605,6 +622,8 @@ class Model(AltersData, metaclass=ModelBase):
|
|||
new = cls(*values)
|
||||
new._state.adding = False
|
||||
new._state.db = db
|
||||
if fetch_mode is not None:
|
||||
new._state.fetch_mode = fetch_mode
|
||||
return new
|
||||
|
||||
def __repr__(self):
|
||||
|
|
@ -714,8 +733,8 @@ class Model(AltersData, metaclass=ModelBase):
|
|||
should be an iterable of field attnames. If fields is None, then
|
||||
all non-deferred fields are reloaded.
|
||||
|
||||
When accessing deferred fields of an instance, the deferred loading
|
||||
of the field will call this method.
|
||||
When fetching deferred fields for a single instance (the FETCH_ONE
|
||||
fetch mode), the deferred loading uses this method.
|
||||
"""
|
||||
if fields is None:
|
||||
self._prefetched_objects_cache = {}
|
||||
|
|
@ -1182,8 +1201,9 @@ class Model(AltersData, metaclass=ModelBase):
|
|||
returning_fields,
|
||||
):
|
||||
"""
|
||||
Try to update the model. Return True if the model was updated (if an
|
||||
update query was done and a matching row was found in the DB).
|
||||
Try to update the model. Return a list of updated fields if the model
|
||||
was updated (if an update query was done and a matching row was
|
||||
found in the DB).
|
||||
"""
|
||||
filtered = base_qs.filter(pk=pk_val)
|
||||
if not values:
|
||||
|
|
@ -1750,6 +1770,7 @@ class Model(AltersData, metaclass=ModelBase):
|
|||
*cls._check_fields(**kwargs),
|
||||
*cls._check_m2m_through_same_relationship(),
|
||||
*cls._check_long_column_names(databases),
|
||||
*cls._check_related_fields(),
|
||||
]
|
||||
clash_errors = (
|
||||
*cls._check_id_field(),
|
||||
|
|
@ -2220,6 +2241,20 @@ class Model(AltersData, metaclass=ModelBase):
|
|||
id="models.E048",
|
||||
)
|
||||
)
|
||||
elif (
|
||||
isinstance(field.remote_field, ForeignObjectRel)
|
||||
and field not in cls._meta.local_concrete_fields
|
||||
and len(field.from_fields) > 1
|
||||
):
|
||||
errors.append(
|
||||
checks.Error(
|
||||
f"{option!r} refers to a ForeignObject {field_name!r} with "
|
||||
"multiple 'from_fields', which is not supported for that "
|
||||
"option.",
|
||||
obj=cls,
|
||||
id="models.E049",
|
||||
)
|
||||
)
|
||||
elif field not in cls._meta.local_fields:
|
||||
errors.append(
|
||||
checks.Error(
|
||||
|
|
@ -2421,6 +2456,29 @@ class Model(AltersData, metaclass=ModelBase):
|
|||
|
||||
return errors
|
||||
|
||||
@classmethod
|
||||
def _check_related_fields(cls):
|
||||
has_db_variant = False
|
||||
has_python_variant = False
|
||||
for rel in cls._meta.get_fields():
|
||||
if rel.related_model:
|
||||
if not (on_delete := getattr(rel.remote_field, "on_delete", None)):
|
||||
continue
|
||||
if isinstance(on_delete, DatabaseOnDelete):
|
||||
has_db_variant = True
|
||||
elif on_delete != DO_NOTHING:
|
||||
has_python_variant = True
|
||||
if has_db_variant and has_python_variant:
|
||||
return [
|
||||
checks.Error(
|
||||
"The model cannot have related fields with both "
|
||||
"database-level and Python-level on_delete variants.",
|
||||
obj=cls,
|
||||
id="models.E050",
|
||||
)
|
||||
]
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
def _get_expr_references(cls, expr):
|
||||
if isinstance(expr, Q):
|
||||
|
|
|
|||
|
|
@ -81,6 +81,28 @@ def DO_NOTHING(collector, field, sub_objs, using):
|
|||
pass
|
||||
|
||||
|
||||
class DatabaseOnDelete:
|
||||
def __init__(self, operation, name, forced_collector=None):
|
||||
self.operation = operation
|
||||
self.forced_collector = forced_collector
|
||||
self.__name__ = name
|
||||
|
||||
__call__ = DO_NOTHING
|
||||
|
||||
def on_delete_sql(self, schema_editor):
|
||||
return schema_editor.connection.ops.fk_on_delete_sql(self.operation)
|
||||
|
||||
def __str__(self):
|
||||
return self.__name__
|
||||
|
||||
|
||||
DB_CASCADE = DatabaseOnDelete("CASCADE", "DB_CASCADE", CASCADE)
|
||||
DB_SET_DEFAULT = DatabaseOnDelete("SET DEFAULT", "DB_SET_DEFAULT")
|
||||
DB_SET_NULL = DatabaseOnDelete("SET NULL", "DB_SET_NULL")
|
||||
|
||||
SKIP_COLLECTION = frozenset([DO_NOTHING, DB_CASCADE, DB_SET_DEFAULT, DB_SET_NULL])
|
||||
|
||||
|
||||
def get_candidate_relations_to_delete(opts):
|
||||
# The candidate relations are the ones that come from N-1 and 1-1
|
||||
# relations. N-N (i.e., many-to-many) relations aren't candidates for
|
||||
|
|
@ -93,10 +115,12 @@ def get_candidate_relations_to_delete(opts):
|
|||
|
||||
|
||||
class Collector:
|
||||
def __init__(self, using, origin=None):
|
||||
def __init__(self, using, origin=None, force_collection=False):
|
||||
self.using = using
|
||||
# A Model or QuerySet object.
|
||||
self.origin = origin
|
||||
# Force collecting objects for deletion on the Python-level.
|
||||
self.force_collection = force_collection
|
||||
# Initially, {model: {instances}}, later values become lists.
|
||||
self.data = defaultdict(set)
|
||||
# {(field, value): [instances, …]}
|
||||
|
|
@ -194,6 +218,8 @@ class Collector:
|
|||
skipping parent -> child -> parent chain preventing fast delete of
|
||||
the child.
|
||||
"""
|
||||
if self.force_collection:
|
||||
return False
|
||||
if from_field and from_field.remote_field.on_delete is not CASCADE:
|
||||
return False
|
||||
if hasattr(objs, "_meta"):
|
||||
|
|
@ -215,7 +241,7 @@ class Collector:
|
|||
and
|
||||
# Foreign keys pointing to this model.
|
||||
all(
|
||||
related.field.remote_field.on_delete is DO_NOTHING
|
||||
related.field.remote_field.on_delete in SKIP_COLLECTION
|
||||
for related in get_candidate_relations_to_delete(opts)
|
||||
)
|
||||
and (
|
||||
|
|
@ -309,12 +335,20 @@ class Collector:
|
|||
protected_objects = defaultdict(list)
|
||||
for related in get_candidate_relations_to_delete(model._meta):
|
||||
# Preserve parent reverse relationships if keep_parents=True.
|
||||
if keep_parents and related.model in model._meta.all_parents:
|
||||
if (
|
||||
keep_parents
|
||||
and related.model._meta.concrete_model in model._meta.all_parents
|
||||
):
|
||||
continue
|
||||
field = related.field
|
||||
on_delete = field.remote_field.on_delete
|
||||
if on_delete == DO_NOTHING:
|
||||
continue
|
||||
if on_delete in SKIP_COLLECTION:
|
||||
if self.force_collection and (
|
||||
forced_on_delete := getattr(on_delete, "forced_collector", None)
|
||||
):
|
||||
on_delete = forced_on_delete
|
||||
else:
|
||||
continue
|
||||
related_model = related.related_model
|
||||
if self.can_fast_delete(related_model, from_field=field):
|
||||
model_fast_deletes[related_model].append(field)
|
||||
|
|
|
|||
61
django/db/models/fetch_modes.py
Normal file
61
django/db/models/fetch_modes.py
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
from django.core.exceptions import FieldFetchBlocked
|
||||
|
||||
|
||||
class FetchMode:
|
||||
__slots__ = ()
|
||||
|
||||
track_peers = False
|
||||
|
||||
def fetch(self, fetcher, instance):
|
||||
raise NotImplementedError("Subclasses must implement this method.")
|
||||
|
||||
|
||||
class FetchOne(FetchMode):
|
||||
__slots__ = ()
|
||||
|
||||
def fetch(self, fetcher, instance):
|
||||
fetcher.fetch_one(instance)
|
||||
|
||||
def __reduce__(self):
|
||||
return "FETCH_ONE"
|
||||
|
||||
|
||||
FETCH_ONE = FetchOne()
|
||||
|
||||
|
||||
class FetchPeers(FetchMode):
|
||||
__slots__ = ()
|
||||
|
||||
track_peers = True
|
||||
|
||||
def fetch(self, fetcher, instance):
|
||||
instances = [
|
||||
peer
|
||||
for peer_weakref in instance._state.peers
|
||||
if (peer := peer_weakref()) is not None
|
||||
]
|
||||
if len(instances) > 1:
|
||||
fetcher.fetch_many(instances)
|
||||
else:
|
||||
fetcher.fetch_one(instance)
|
||||
|
||||
def __reduce__(self):
|
||||
return "FETCH_PEERS"
|
||||
|
||||
|
||||
FETCH_PEERS = FetchPeers()
|
||||
|
||||
|
||||
class Raise(FetchMode):
|
||||
__slots__ = ()
|
||||
|
||||
def fetch(self, fetcher, instance):
|
||||
klass = instance.__class__.__qualname__
|
||||
field_name = fetcher.field.name
|
||||
raise FieldFetchBlocked(f"Fetching of {klass}.{field_name} blocked.") from None
|
||||
|
||||
def __reduce__(self):
|
||||
return "RAISE"
|
||||
|
||||
|
||||
RAISE = Raise()
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue