Merge branch 'django:main' into patch-1

This commit is contained in:
ddelange 2025-11-02 18:45:48 +01:00 committed by GitHub
commit c9f6cadba5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
268 changed files with 5801 additions and 1163 deletions

1
.gitattributes vendored
View file

@ -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

View file

@ -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: |

View file

@ -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

View file

@ -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 💪

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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>

View file

@ -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")),

View file

@ -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",

View file

@ -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

View file

@ -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 ""

View file

@ -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,

View file

@ -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

View file

View 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

View file

@ -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);
}

View file

@ -357,7 +357,7 @@ body.popup .submit-row {
width: 48em;
}
.flatpages-flatpage #id_content {
.app-flatpages.model-flatpage #id_content {
height: 40.2em;
}

View file

@ -2,10 +2,10 @@
{% load i18n %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
&rsaquo; {% 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 %}

View file

@ -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>
&rsaquo;
{% 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 %}

View file

@ -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>
&rsaquo; <a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a>
&rsaquo; <a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>
&rsaquo; <a href="{% url opts|admin_urlname:'change' original.pk|admin_urlquote %}">{{ original|truncatewords:"18" }}</a>
&rsaquo; {% 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">

View file

@ -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 %} &rsaquo; {{ 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 %}

View file

@ -15,12 +15,12 @@
{% if not is_popup %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
&rsaquo; <a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a>
&rsaquo; {% if has_view_permission %}<a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>{% else %}{{ opts.verbose_name_plural|capfirst }}{% endif %}
&rsaquo; {% 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 %}

View file

@ -29,11 +29,11 @@
{% if not is_popup %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
&rsaquo; <a href="{% url 'admin:app_list' app_label=cl.opts.app_label %}">{{ cl.opts.app_config.verbose_name }}</a>
&rsaquo; {{ 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 %}

View file

@ -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>
&rsaquo; <a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a>
&rsaquo; <a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>
&rsaquo; <a href="{% url opts|admin_urlname:'change' object.pk|admin_urlquote %}">{{ object|truncatewords:"18" }}</a>
&rsaquo; {% 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 %}

View file

@ -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>
&rsaquo; <a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a>
&rsaquo; <a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>
&rsaquo; {% 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 %}

View file

@ -2,10 +2,10 @@
{% load i18n %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
&rsaquo; {{ 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 %}

View file

@ -2,13 +2,13 @@
{% load i18n admin_urls %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
&rsaquo; <a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a>
&rsaquo; <a href="{% url opts|admin_urlname:'changelist' %}">{{ module_name }}</a>
&rsaquo; <a href="{% url opts|admin_urlname:'change' object.pk|admin_urlquote %}">{{ object|truncatewords:"18" }}</a>
&rsaquo; {% 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 %}

View file

@ -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 %}

View file

@ -9,10 +9,10 @@
{% include "admin/color_theme_toggle.html" %}
{% endblock %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
&rsaquo; {% 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 %}

View file

@ -12,10 +12,10 @@
{% include "admin/color_theme_toggle.html" %}
{% endblock %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
&rsaquo; {% 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">

View file

@ -2,10 +2,10 @@
{% load i18n %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
&rsaquo; {% 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 %}

View file

@ -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>
&rsaquo; {% 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 %}

View file

@ -2,10 +2,10 @@
{% load i18n %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
&rsaquo; {% 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 %}

View file

@ -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>
&rsaquo; {% 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 %}

View file

@ -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):
"""

View file

@ -2,11 +2,11 @@
{% load i18n %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
&rsaquo; <a href="{% url 'django-admindocs-docroot' %}">{% translate 'Documentation' %}</a>
&rsaquo; {% 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 %}

View file

@ -2,10 +2,10 @@
{% load i18n %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
&rsaquo; {% 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 %}

View file

@ -2,10 +2,10 @@
{% load i18n %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
&rsaquo; {% 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 %}

View file

@ -10,12 +10,12 @@
{% endblock %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
&rsaquo; <a href="{% url 'django-admindocs-docroot' %}">{% translate 'Documentation' %}</a>
&rsaquo; <a href="{% url 'django-admindocs-models-index' %}">{% translate 'Models' %}</a>
&rsaquo; {{ 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 %}

View file

@ -4,11 +4,11 @@
{% block coltype %}colSM{% endblock %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
&rsaquo; <a href="{% url 'django-admindocs-docroot' %}">{% translate 'Documentation' %}</a>
&rsaquo; {% 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 %}

View file

@ -2,12 +2,12 @@
{% load i18n %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
&rsaquo; <a href="{% url 'django-admindocs-docroot' %}">{% translate 'Documentation' %}</a>
&rsaquo; {% translate 'Templates' %}
&rsaquo; {{ 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 %}

View file

@ -3,11 +3,11 @@
{% block coltype %}colSM{% endblock %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
&rsaquo; <a href="{% url 'django-admindocs-docroot' %}">{% translate 'Documentation' %}</a>
&rsaquo; {% 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 %}

View file

@ -3,11 +3,11 @@
{% block coltype %}colSM{% endblock %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
&rsaquo; <a href="{% url 'django-admindocs-docroot' %}">{% translate 'Documentation' %}</a>
&rsaquo; {% 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 %}

View file

@ -2,12 +2,12 @@
{% load i18n %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
&rsaquo; <a href="{% url 'django-admindocs-docroot' %}">{% translate 'Documentation' %}</a>
&rsaquo; <a href="{% url 'django-admindocs-views-index' %}">{% translate 'Views' %}</a>
&rsaquo; {{ 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 %}

View file

@ -3,11 +3,11 @@
{% block coltype %}colSM{% endblock %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
&rsaquo; <a href="{% url 'django-admindocs-docroot' %}">{% translate 'Documentation' %}</a>
&rsaquo; {% 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 %}

View file

@ -134,6 +134,9 @@ class SetPasswordMixin:
user.save()
return user
def __class_getitem__(cls, *args, **kwargs):
return cls
class SetUnusablePasswordMixin:
"""

View file

@ -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:

View file

@ -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

View file

@ -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 = {

View file

@ -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))

View file

@ -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"]
)

View file

@ -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):

View file

@ -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):

View file

@ -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()

View file

@ -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":

View file

@ -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"]

View file

@ -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__ = [

View file

@ -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 = {

View file

@ -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)

View file

@ -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",

View file

@ -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 "

View file

@ -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):

View file

@ -132,6 +132,12 @@ class FieldError(Exception):
pass
class FieldFetchBlocked(FieldError):
"""On-demand fetching of a model field blocked."""
pass
NON_FIELD_ERRORS = "__all__"

View file

@ -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:

View file

@ -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):

View file

@ -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

View file

@ -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.

View file

@ -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 "

View file

@ -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

View file

@ -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):

View file

@ -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(
"""

View file

@ -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

View file

@ -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):

View file

@ -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)"

View file

@ -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)"

View file

@ -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

View file

@ -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).

View file

@ -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):

View file

@ -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

View file

@ -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"

View file

@ -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,
}

View file

@ -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",

View file

@ -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")
)

View file

@ -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],
)

View file

@ -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

View file

@ -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.

View file

@ -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()

View file

@ -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")
)

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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",

View file

@ -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):

View file

@ -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)

View 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