mirror of
https://github.com/django/django.git
synced 2025-06-05 11:39:13 +00:00
Merge branch 'django:main' into ticket_35831
This commit is contained in:
commit
ba6a74d058
3
.github/workflows/docs.yml
vendored
3
.github/workflows/docs.yml
vendored
@ -21,8 +21,7 @@ permissions:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
docs:
|
docs:
|
||||||
# OS must be the same as on djangoproject.com.
|
runs-on: ubuntu-24.04
|
||||||
runs-on: ubuntu-20.04
|
|
||||||
name: docs
|
name: docs
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
|
2
.github/workflows/python_matrix.yml
vendored
2
.github/workflows/python_matrix.yml
vendored
@ -49,4 +49,4 @@ jobs:
|
|||||||
run: python -m pip install --upgrade pip setuptools wheel
|
run: python -m pip install --upgrade pip setuptools wheel
|
||||||
- run: python -m pip install -r tests/requirements/py3.txt -e .
|
- run: python -m pip install -r tests/requirements/py3.txt -e .
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: python tests/runtests.py -v2
|
run: python -Wall tests/runtests.py -v2
|
||||||
|
5
.github/workflows/schedule_tests.yml
vendored
5
.github/workflows/schedule_tests.yml
vendored
@ -20,6 +20,7 @@ jobs:
|
|||||||
- '3.11'
|
- '3.11'
|
||||||
- '3.12'
|
- '3.12'
|
||||||
- '3.13'
|
- '3.13'
|
||||||
|
- '3.14-dev'
|
||||||
name: Windows, SQLite, Python ${{ matrix.python-version }}
|
name: Windows, SQLite, Python ${{ matrix.python-version }}
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
steps:
|
steps:
|
||||||
@ -35,7 +36,7 @@ jobs:
|
|||||||
run: python -m pip install --upgrade pip setuptools wheel
|
run: python -m pip install --upgrade pip setuptools wheel
|
||||||
- run: python -m pip install -r tests/requirements/py3.txt -e .
|
- run: python -m pip install -r tests/requirements/py3.txt -e .
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: python tests/runtests.py -v2
|
run: python -Wall tests/runtests.py -v2
|
||||||
|
|
||||||
pyc-only:
|
pyc-only:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@ -61,7 +62,7 @@ jobs:
|
|||||||
find $DJANGO_PACKAGE_ROOT -name '*.py' -print -delete
|
find $DJANGO_PACKAGE_ROOT -name '*.py' -print -delete
|
||||||
- run: python -m pip install -r tests/requirements/py3.txt
|
- run: python -m pip install -r tests/requirements/py3.txt
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: python tests/runtests.py --verbosity=2
|
run: python -Wall tests/runtests.py --verbosity=2
|
||||||
|
|
||||||
pypy-sqlite:
|
pypy-sqlite:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
@ -38,7 +38,7 @@ jobs:
|
|||||||
run: python -m pip install --upgrade pip setuptools wheel
|
run: python -m pip install --upgrade pip setuptools wheel
|
||||||
- run: python -m pip install -r tests/requirements/py3.txt -e .
|
- run: python -m pip install -r tests/requirements/py3.txt -e .
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: python tests/runtests.py -v2
|
run: python -Wall tests/runtests.py -v2
|
||||||
|
|
||||||
javascript-tests:
|
javascript-tests:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
@ -4,12 +4,13 @@
|
|||||||
version: 2
|
version: 2
|
||||||
|
|
||||||
build:
|
build:
|
||||||
os: ubuntu-20.04
|
os: ubuntu-24.04
|
||||||
tools:
|
tools:
|
||||||
python: "3.8"
|
python: "3.12"
|
||||||
|
|
||||||
sphinx:
|
sphinx:
|
||||||
configuration: docs/conf.py
|
configuration: docs/conf.py
|
||||||
|
fail_on_warning: true
|
||||||
|
|
||||||
python:
|
python:
|
||||||
install:
|
install:
|
||||||
|
1
AUTHORS
1
AUTHORS
@ -282,6 +282,7 @@ answer newbie questions, and generally made Django that much better:
|
|||||||
David Sanders <dsanders11@ucsbalum.com>
|
David Sanders <dsanders11@ucsbalum.com>
|
||||||
David Schein
|
David Schein
|
||||||
David Tulig <david.tulig@gmail.com>
|
David Tulig <david.tulig@gmail.com>
|
||||||
|
David Winiecki <david.winiecki@gmail.com>
|
||||||
David Winterbottom <david.winterbottom@gmail.com>
|
David Winterbottom <david.winterbottom@gmail.com>
|
||||||
David Wobrock <david.wobrock@gmail.com>
|
David Wobrock <david.wobrock@gmail.com>
|
||||||
Davide Ceretti <dav.ceretti@gmail.com>
|
Davide Ceretti <dav.ceretti@gmail.com>
|
||||||
|
@ -121,7 +121,7 @@ class Fieldset:
|
|||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def is_collapsible(self):
|
def is_collapsible(self):
|
||||||
if any([field in self.fields for field in self.form.errors]):
|
if any(field in self.fields for field in self.form.errors):
|
||||||
return False
|
return False
|
||||||
return "collapse" in self.classes
|
return "collapse" in self.classes
|
||||||
|
|
||||||
|
@ -41,6 +41,7 @@ from django.core.exceptions import (
|
|||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
from django.db import models, router, transaction
|
from django.db import models, router, transaction
|
||||||
from django.db.models.constants import LOOKUP_SEP
|
from django.db.models.constants import LOOKUP_SEP
|
||||||
|
from django.db.models.functions import Cast
|
||||||
from django.forms.formsets import DELETION_FIELD_NAME, all_valid
|
from django.forms.formsets import DELETION_FIELD_NAME, all_valid
|
||||||
from django.forms.models import (
|
from django.forms.models import (
|
||||||
BaseInlineFormSet,
|
BaseInlineFormSet,
|
||||||
@ -1177,17 +1178,17 @@ class ModelAdmin(BaseModelAdmin):
|
|||||||
# Apply keyword searches.
|
# Apply keyword searches.
|
||||||
def construct_search(field_name):
|
def construct_search(field_name):
|
||||||
if field_name.startswith("^"):
|
if field_name.startswith("^"):
|
||||||
return "%s__istartswith" % field_name.removeprefix("^")
|
return "%s__istartswith" % field_name.removeprefix("^"), None
|
||||||
elif field_name.startswith("="):
|
elif field_name.startswith("="):
|
||||||
return "%s__iexact" % field_name.removeprefix("=")
|
return "%s__iexact" % field_name.removeprefix("="), None
|
||||||
elif field_name.startswith("@"):
|
elif field_name.startswith("@"):
|
||||||
return "%s__search" % field_name.removeprefix("@")
|
return "%s__search" % field_name.removeprefix("@"), None
|
||||||
# Use field_name if it includes a lookup.
|
# Use field_name if it includes a lookup.
|
||||||
opts = queryset.model._meta
|
opts = queryset.model._meta
|
||||||
lookup_fields = field_name.split(LOOKUP_SEP)
|
lookup_fields = field_name.split(LOOKUP_SEP)
|
||||||
# Go through the fields, following all relations.
|
# Go through the fields, following all relations.
|
||||||
prev_field = None
|
prev_field = None
|
||||||
for path_part in lookup_fields:
|
for i, path_part in enumerate(lookup_fields):
|
||||||
if path_part == "pk":
|
if path_part == "pk":
|
||||||
path_part = opts.pk.name
|
path_part = opts.pk.name
|
||||||
try:
|
try:
|
||||||
@ -1195,21 +1196,40 @@ class ModelAdmin(BaseModelAdmin):
|
|||||||
except FieldDoesNotExist:
|
except FieldDoesNotExist:
|
||||||
# Use valid query lookups.
|
# Use valid query lookups.
|
||||||
if prev_field and prev_field.get_lookup(path_part):
|
if prev_field and prev_field.get_lookup(path_part):
|
||||||
return field_name
|
if path_part == "exact" and not isinstance(
|
||||||
|
prev_field, (models.CharField, models.TextField)
|
||||||
|
):
|
||||||
|
field_name_without_exact = "__".join(lookup_fields[:i])
|
||||||
|
alias = Cast(
|
||||||
|
field_name_without_exact,
|
||||||
|
output_field=models.CharField(),
|
||||||
|
)
|
||||||
|
alias_name = "_".join(lookup_fields[:i])
|
||||||
|
return f"{alias_name}_str", alias
|
||||||
|
else:
|
||||||
|
return field_name, None
|
||||||
else:
|
else:
|
||||||
prev_field = field
|
prev_field = field
|
||||||
if hasattr(field, "path_infos"):
|
if hasattr(field, "path_infos"):
|
||||||
# Update opts to follow the relation.
|
# Update opts to follow the relation.
|
||||||
opts = field.path_infos[-1].to_opts
|
opts = field.path_infos[-1].to_opts
|
||||||
# Otherwise, use the field with icontains.
|
# Otherwise, use the field with icontains.
|
||||||
return "%s__icontains" % field_name
|
return "%s__icontains" % field_name, None
|
||||||
|
|
||||||
may_have_duplicates = False
|
may_have_duplicates = False
|
||||||
search_fields = self.get_search_fields(request)
|
search_fields = self.get_search_fields(request)
|
||||||
if search_fields and search_term:
|
if search_fields and search_term:
|
||||||
orm_lookups = [
|
str_aliases = {}
|
||||||
construct_search(str(search_field)) for search_field in search_fields
|
orm_lookups = []
|
||||||
]
|
for field in search_fields:
|
||||||
|
lookup, str_alias = construct_search(str(field))
|
||||||
|
orm_lookups.append(lookup)
|
||||||
|
if str_alias:
|
||||||
|
str_aliases[lookup] = str_alias
|
||||||
|
|
||||||
|
if str_aliases:
|
||||||
|
queryset = queryset.alias(**str_aliases)
|
||||||
|
|
||||||
term_queries = []
|
term_queries = []
|
||||||
for bit in smart_split(search_term):
|
for bit in smart_split(search_term):
|
||||||
if bit.startswith(('"', "'")) and bit[0] == bit[-1]:
|
if bit.startswith(('"', "'")) and bit[0] == bit[-1]:
|
||||||
|
@ -282,7 +282,7 @@ class AdminSite:
|
|||||||
path("autocomplete/", wrap(self.autocomplete_view), name="autocomplete"),
|
path("autocomplete/", wrap(self.autocomplete_view), name="autocomplete"),
|
||||||
path("jsi18n/", wrap(self.i18n_javascript, cacheable=True), name="jsi18n"),
|
path("jsi18n/", wrap(self.i18n_javascript, cacheable=True), name="jsi18n"),
|
||||||
path(
|
path(
|
||||||
"r/<int:content_type_id>/<path:object_id>/",
|
"r/<path:content_type_id>/<path:object_id>/",
|
||||||
wrap(contenttype_views.shortcut),
|
wrap(contenttype_views.shortcut),
|
||||||
name="view_on_site",
|
name="view_on_site",
|
||||||
),
|
),
|
||||||
|
@ -299,7 +299,7 @@ input[type="submit"], button {
|
|||||||
background-position: 0 -80px;
|
background-position: 0 -80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
a.selector-chooseall, a.selector-clearall {
|
.selector-chooseall, .selector-clearall {
|
||||||
align-self: center;
|
align-self: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -649,6 +649,7 @@ input[type="submit"], button {
|
|||||||
|
|
||||||
.related-widget-wrapper .selector {
|
.related-widget-wrapper .selector {
|
||||||
order: 1;
|
order: 1;
|
||||||
|
flex: 1 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.related-widget-wrapper > a {
|
.related-widget-wrapper > a {
|
||||||
|
@ -235,19 +235,19 @@ fieldset .fieldBox {
|
|||||||
background-position: 0 -112px;
|
background-position: 0 -112px;
|
||||||
}
|
}
|
||||||
|
|
||||||
a.selector-chooseall {
|
.selector-chooseall {
|
||||||
background: url(../img/selector-icons.svg) right -128px no-repeat;
|
background: url(../img/selector-icons.svg) right -128px no-repeat;
|
||||||
}
|
}
|
||||||
|
|
||||||
a.active.selector-chooseall:focus, a.active.selector-chooseall:hover {
|
.active.selector-chooseall:focus, .active.selector-chooseall:hover {
|
||||||
background-position: 100% -144px;
|
background-position: 100% -144px;
|
||||||
}
|
}
|
||||||
|
|
||||||
a.selector-clearall {
|
.selector-clearall {
|
||||||
background: url(../img/selector-icons.svg) 0 -160px no-repeat;
|
background: url(../img/selector-icons.svg) 0 -160px no-repeat;
|
||||||
}
|
}
|
||||||
|
|
||||||
a.active.selector-clearall:focus, a.active.selector-clearall:hover {
|
.active.selector-clearall:focus, .active.selector-clearall:hover {
|
||||||
background-position: 0 -176px;
|
background-position: 0 -176px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
.selector {
|
.selector {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-grow: 1;
|
flex: 1;
|
||||||
gap: 0 10px;
|
gap: 0 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -14,17 +14,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.selector-available, .selector-chosen {
|
.selector-available, .selector-chosen {
|
||||||
text-align: center;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex: 1 1;
|
flex: 1 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.selector-available h2, .selector-chosen h2 {
|
.selector-available-title, .selector-chosen-title {
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: 4px 4px 0 0;
|
border-radius: 4px 4px 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.selector .helptext {
|
||||||
|
font-size: 0.6875rem;
|
||||||
|
}
|
||||||
|
|
||||||
.selector-chosen .list-footer-display {
|
.selector-chosen .list-footer-display {
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-top: none;
|
border-top: none;
|
||||||
@ -40,14 +43,20 @@
|
|||||||
color: var(--breadcrumbs-fg);
|
color: var(--breadcrumbs-fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.selector-chosen h2 {
|
.selector-chosen-title {
|
||||||
background: var(--secondary);
|
background: var(--secondary);
|
||||||
color: var(--header-link-color);
|
color: var(--header-link-color);
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector-chosen-title label {
|
||||||
|
color: var(--header-link-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.selector .selector-available h2 {
|
.selector-available-title {
|
||||||
background: var(--darkened-bg);
|
background: var(--darkened-bg);
|
||||||
color: var(--body-quiet-color);
|
color: var(--body-quiet-color);
|
||||||
|
padding: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.selector .selector-filter {
|
.selector .selector-filter {
|
||||||
@ -121,6 +130,7 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
opacity: 0.55;
|
opacity: 0.55;
|
||||||
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.active.selector-add, .active.selector-remove {
|
.active.selector-add, .active.selector-remove {
|
||||||
@ -147,7 +157,7 @@
|
|||||||
background-position: 0 -80px;
|
background-position: 0 -80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
a.selector-chooseall, a.selector-clearall {
|
.selector-chooseall, .selector-clearall {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
@ -158,38 +168,39 @@ a.selector-chooseall, a.selector-clearall {
|
|||||||
color: var(--body-quiet-color);
|
color: var(--body-quiet-color);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
opacity: 0.55;
|
opacity: 0.55;
|
||||||
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
a.active.selector-chooseall:focus, a.active.selector-clearall:focus,
|
.active.selector-chooseall:focus, .active.selector-clearall:focus,
|
||||||
a.active.selector-chooseall:hover, a.active.selector-clearall:hover {
|
.active.selector-chooseall:hover, .active.selector-clearall:hover {
|
||||||
color: var(--link-fg);
|
color: var(--link-fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
a.active.selector-chooseall, a.active.selector-clearall {
|
.active.selector-chooseall, .active.selector-clearall {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
a.active.selector-chooseall:hover, a.active.selector-clearall:hover {
|
.active.selector-chooseall:hover, .active.selector-clearall:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
a.selector-chooseall {
|
.selector-chooseall {
|
||||||
padding: 0 18px 0 0;
|
padding: 0 18px 0 0;
|
||||||
background: url(../img/selector-icons.svg) right -160px no-repeat;
|
background: url(../img/selector-icons.svg) right -160px no-repeat;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
a.active.selector-chooseall:focus, a.active.selector-chooseall:hover {
|
.active.selector-chooseall:focus, .active.selector-chooseall:hover {
|
||||||
background-position: 100% -176px;
|
background-position: 100% -176px;
|
||||||
}
|
}
|
||||||
|
|
||||||
a.selector-clearall {
|
.selector-clearall {
|
||||||
padding: 0 0 0 18px;
|
padding: 0 0 0 18px;
|
||||||
background: url(../img/selector-icons.svg) 0 -128px no-repeat;
|
background: url(../img/selector-icons.svg) 0 -128px no-repeat;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
a.active.selector-clearall:focus, a.active.selector-clearall:hover {
|
.active.selector-clearall:focus, .active.selector-clearall:hover {
|
||||||
background-position: 0 -144px;
|
background-position: 0 -144px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@ Requires core.js and SelectBox.js.
|
|||||||
const from_box = document.getElementById(field_id);
|
const from_box = document.getElementById(field_id);
|
||||||
from_box.id += '_from'; // change its ID
|
from_box.id += '_from'; // change its ID
|
||||||
from_box.className = 'filtered';
|
from_box.className = 'filtered';
|
||||||
|
from_box.setAttribute('aria-labelledby', field_id + '_from_title');
|
||||||
|
|
||||||
for (const p of from_box.parentNode.getElementsByTagName('p')) {
|
for (const p of from_box.parentNode.getElementsByTagName('p')) {
|
||||||
if (p.classList.contains("info")) {
|
if (p.classList.contains("info")) {
|
||||||
@ -38,18 +39,15 @@ Requires core.js and SelectBox.js.
|
|||||||
// <div class="selector-available">
|
// <div class="selector-available">
|
||||||
const selector_available = quickElement('div', selector_div);
|
const selector_available = quickElement('div', selector_div);
|
||||||
selector_available.className = 'selector-available';
|
selector_available.className = 'selector-available';
|
||||||
const title_available = quickElement('h2', selector_available, interpolate(gettext('Available %s') + ' ', [field_name]));
|
const selector_available_title = quickElement('div', selector_available);
|
||||||
|
selector_available_title.id = field_id + '_from_title';
|
||||||
|
selector_available_title.className = 'selector-available-title';
|
||||||
|
quickElement('label', selector_available_title, interpolate(gettext('Available %s') + ' ', [field_name]), 'for', field_id + '_from');
|
||||||
quickElement(
|
quickElement(
|
||||||
'span', title_available, '',
|
'p',
|
||||||
'class', 'help help-tooltip help-icon',
|
selector_available_title,
|
||||||
'title', interpolate(
|
interpolate(gettext('Choose %s by selecting them and then select the "Choose" arrow button.'), [field_name]),
|
||||||
gettext(
|
'class', 'helptext'
|
||||||
'This is the list of available %s. You may choose some by ' +
|
|
||||||
'selecting them in the box below and then clicking the ' +
|
|
||||||
'"Choose" arrow between the two boxes.'
|
|
||||||
),
|
|
||||||
[field_name]
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const filter_p = quickElement('p', selector_available, '', 'id', field_id + '_filter');
|
const filter_p = quickElement('p', selector_available, '', 'id', field_id + '_filter');
|
||||||
@ -60,7 +58,7 @@ Requires core.js and SelectBox.js.
|
|||||||
quickElement(
|
quickElement(
|
||||||
'span', search_filter_label, '',
|
'span', search_filter_label, '',
|
||||||
'class', 'help-tooltip search-label-icon',
|
'class', 'help-tooltip search-label-icon',
|
||||||
'title', interpolate(gettext("Type into this box to filter down the list of available %s."), [field_name])
|
'aria-label', interpolate(gettext("Type into this box to filter down the list of available %s."), [field_name])
|
||||||
);
|
);
|
||||||
|
|
||||||
filter_p.appendChild(document.createTextNode(' '));
|
filter_p.appendChild(document.createTextNode(' '));
|
||||||
@ -69,32 +67,44 @@ Requires core.js and SelectBox.js.
|
|||||||
filter_input.id = field_id + '_input';
|
filter_input.id = field_id + '_input';
|
||||||
|
|
||||||
selector_available.appendChild(from_box);
|
selector_available.appendChild(from_box);
|
||||||
const choose_all = quickElement('a', selector_available, gettext('Choose all'), 'title', interpolate(gettext('Click to choose all %s at once.'), [field_name]), 'href', '#', 'id', field_id + '_add_all_link');
|
const choose_all = quickElement(
|
||||||
choose_all.className = 'selector-chooseall';
|
'button',
|
||||||
|
selector_available,
|
||||||
|
interpolate(gettext('Choose all %s'), [field_name]),
|
||||||
|
'id', field_id + '_add_all',
|
||||||
|
'class', 'selector-chooseall'
|
||||||
|
);
|
||||||
|
|
||||||
// <ul class="selector-chooser">
|
// <ul class="selector-chooser">
|
||||||
const selector_chooser = quickElement('ul', selector_div);
|
const selector_chooser = quickElement('ul', selector_div);
|
||||||
selector_chooser.className = 'selector-chooser';
|
selector_chooser.className = 'selector-chooser';
|
||||||
const add_link = quickElement('a', quickElement('li', selector_chooser), gettext('Choose'), 'title', gettext('Choose'), 'href', '#', 'id', field_id + '_add_link');
|
const add_button = quickElement(
|
||||||
add_link.className = 'selector-add';
|
'button',
|
||||||
const remove_link = quickElement('a', quickElement('li', selector_chooser), gettext('Remove'), 'title', gettext('Remove'), 'href', '#', 'id', field_id + '_remove_link');
|
quickElement('li', selector_chooser),
|
||||||
remove_link.className = 'selector-remove';
|
interpolate(gettext('Choose selected %s'), [field_name]),
|
||||||
|
'id', field_id + '_add',
|
||||||
|
'class', 'selector-add'
|
||||||
|
);
|
||||||
|
const remove_button = quickElement(
|
||||||
|
'button',
|
||||||
|
quickElement('li', selector_chooser),
|
||||||
|
interpolate(gettext('Remove selected chosen %s'), [field_name]),
|
||||||
|
'id', field_id + '_remove',
|
||||||
|
'class', 'selector-remove'
|
||||||
|
);
|
||||||
|
|
||||||
// <div class="selector-chosen">
|
// <div class="selector-chosen">
|
||||||
const selector_chosen = quickElement('div', selector_div, '', 'id', field_id + '_selector_chosen');
|
const selector_chosen = quickElement('div', selector_div, '', 'id', field_id + '_selector_chosen');
|
||||||
selector_chosen.className = 'selector-chosen';
|
selector_chosen.className = 'selector-chosen';
|
||||||
const title_chosen = quickElement('h2', selector_chosen, interpolate(gettext('Chosen %s') + ' ', [field_name]));
|
const selector_chosen_title = quickElement('div', selector_chosen);
|
||||||
|
selector_chosen_title.className = 'selector-chosen-title';
|
||||||
|
selector_chosen_title.id = field_id + '_to_title';
|
||||||
|
quickElement('label', selector_chosen_title, interpolate(gettext('Chosen %s') + ' ', [field_name]), 'for', field_id + '_to');
|
||||||
quickElement(
|
quickElement(
|
||||||
'span', title_chosen, '',
|
'p',
|
||||||
'class', 'help help-tooltip help-icon',
|
selector_chosen_title,
|
||||||
'title', interpolate(
|
interpolate(gettext('Remove %s by selecting them and then select the "Remove" arrow button.'), [field_name]),
|
||||||
gettext(
|
'class', 'helptext'
|
||||||
'This is the list of chosen %s. You may remove some by ' +
|
|
||||||
'selecting them in the box below and then clicking the ' +
|
|
||||||
'"Remove" arrow between the two boxes.'
|
|
||||||
),
|
|
||||||
[field_name]
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const filter_selected_p = quickElement('p', selector_chosen, '', 'id', field_id + '_filter_selected');
|
const filter_selected_p = quickElement('p', selector_chosen, '', 'id', field_id + '_filter_selected');
|
||||||
@ -105,7 +115,7 @@ Requires core.js and SelectBox.js.
|
|||||||
quickElement(
|
quickElement(
|
||||||
'span', search_filter_selected_label, '',
|
'span', search_filter_selected_label, '',
|
||||||
'class', 'help-tooltip search-label-icon',
|
'class', 'help-tooltip search-label-icon',
|
||||||
'title', interpolate(gettext("Type into this box to filter down the list of selected %s."), [field_name])
|
'aria-label', interpolate(gettext("Type into this box to filter down the list of selected %s."), [field_name])
|
||||||
);
|
);
|
||||||
|
|
||||||
filter_selected_p.appendChild(document.createTextNode(' '));
|
filter_selected_p.appendChild(document.createTextNode(' '));
|
||||||
@ -113,15 +123,27 @@ Requires core.js and SelectBox.js.
|
|||||||
const filter_selected_input = quickElement('input', filter_selected_p, '', 'type', 'text', 'placeholder', gettext("Filter"));
|
const filter_selected_input = quickElement('input', filter_selected_p, '', 'type', 'text', 'placeholder', gettext("Filter"));
|
||||||
filter_selected_input.id = field_id + '_selected_input';
|
filter_selected_input.id = field_id + '_selected_input';
|
||||||
|
|
||||||
const to_box = quickElement('select', selector_chosen, '', 'id', field_id + '_to', 'multiple', '', 'size', from_box.size, 'name', from_box.name);
|
quickElement(
|
||||||
to_box.className = 'filtered';
|
'select',
|
||||||
|
selector_chosen,
|
||||||
|
'',
|
||||||
|
'id', field_id + '_to',
|
||||||
|
'multiple', '',
|
||||||
|
'size', from_box.size,
|
||||||
|
'name', from_box.name,
|
||||||
|
'aria-labelledby', field_id + '_to_title',
|
||||||
|
'class', 'filtered'
|
||||||
|
);
|
||||||
const warning_footer = quickElement('div', selector_chosen, '', 'class', 'list-footer-display');
|
const warning_footer = quickElement('div', selector_chosen, '', 'class', 'list-footer-display');
|
||||||
quickElement('span', warning_footer, '', 'id', field_id + '_list-footer-display-text');
|
quickElement('span', warning_footer, '', 'id', field_id + '_list-footer-display-text');
|
||||||
quickElement('span', warning_footer, ' ' + gettext('(click to clear)'), 'class', 'list-footer-display__clear');
|
quickElement('span', warning_footer, ' ' + gettext('(click to clear)'), 'class', 'list-footer-display__clear');
|
||||||
|
const clear_all = quickElement(
|
||||||
const clear_all = quickElement('a', selector_chosen, gettext('Remove all'), 'title', interpolate(gettext('Click to remove all chosen %s at once.'), [field_name]), 'href', '#', 'id', field_id + '_remove_all_link');
|
'button',
|
||||||
clear_all.className = 'selector-clearall';
|
selector_chosen,
|
||||||
|
interpolate(gettext('Remove all %s'), [field_name]),
|
||||||
|
'id', field_id + '_remove_all',
|
||||||
|
'class', 'selector-clearall'
|
||||||
|
);
|
||||||
|
|
||||||
from_box.name = from_box.name + '_old';
|
from_box.name = from_box.name + '_old';
|
||||||
|
|
||||||
@ -138,10 +160,10 @@ Requires core.js and SelectBox.js.
|
|||||||
choose_all.addEventListener('click', function(e) {
|
choose_all.addEventListener('click', function(e) {
|
||||||
move_selection(e, this, SelectBox.move_all, field_id + '_from', field_id + '_to');
|
move_selection(e, this, SelectBox.move_all, field_id + '_from', field_id + '_to');
|
||||||
});
|
});
|
||||||
add_link.addEventListener('click', function(e) {
|
add_button.addEventListener('click', function(e) {
|
||||||
move_selection(e, this, SelectBox.move, field_id + '_from', field_id + '_to');
|
move_selection(e, this, SelectBox.move, field_id + '_from', field_id + '_to');
|
||||||
});
|
});
|
||||||
remove_link.addEventListener('click', function(e) {
|
remove_button.addEventListener('click', function(e) {
|
||||||
move_selection(e, this, SelectBox.move, field_id + '_to', field_id + '_from');
|
move_selection(e, this, SelectBox.move, field_id + '_to', field_id + '_from');
|
||||||
});
|
});
|
||||||
clear_all.addEventListener('click', function(e) {
|
clear_all.addEventListener('click', function(e) {
|
||||||
@ -227,11 +249,11 @@ Requires core.js and SelectBox.js.
|
|||||||
const from = document.getElementById(field_id + '_from');
|
const from = document.getElementById(field_id + '_from');
|
||||||
const to = document.getElementById(field_id + '_to');
|
const to = document.getElementById(field_id + '_to');
|
||||||
// Active if at least one item is selected
|
// Active if at least one item is selected
|
||||||
document.getElementById(field_id + '_add_link').classList.toggle('active', SelectFilter.any_selected(from));
|
document.getElementById(field_id + '_add').classList.toggle('active', SelectFilter.any_selected(from));
|
||||||
document.getElementById(field_id + '_remove_link').classList.toggle('active', SelectFilter.any_selected(to));
|
document.getElementById(field_id + '_remove').classList.toggle('active', SelectFilter.any_selected(to));
|
||||||
// Active if the corresponding box isn't empty
|
// Active if the corresponding box isn't empty
|
||||||
document.getElementById(field_id + '_add_all_link').classList.toggle('active', from.querySelector('option'));
|
document.getElementById(field_id + '_add_all').classList.toggle('active', from.querySelector('option'));
|
||||||
document.getElementById(field_id + '_remove_all_link').classList.toggle('active', to.querySelector('option'));
|
document.getElementById(field_id + '_remove_all').classList.toggle('active', to.querySelector('option'));
|
||||||
SelectFilter.refresh_filtered_warning(field_id);
|
SelectFilter.refresh_filtered_warning(field_id);
|
||||||
},
|
},
|
||||||
filter_key_press: function(event, field_id, source, target) {
|
filter_key_press: function(event, field_id, source, target) {
|
||||||
|
@ -50,11 +50,11 @@
|
|||||||
// If forms are laid out as table rows, insert the
|
// If forms are laid out as table rows, insert the
|
||||||
// "add" button in a new table row:
|
// "add" button in a new table row:
|
||||||
const numCols = $this.eq(-1).children().length;
|
const numCols = $this.eq(-1).children().length;
|
||||||
$parent.append('<tr class="' + options.addCssClass + '"><td colspan="' + numCols + '"><a class="addlink" href="#">' + options.addText + "</a></tr>");
|
$parent.append('<tr class="' + options.addCssClass + '"><td colspan="' + numCols + '"><a role="button" class="addlink" href="#">' + options.addText + "</a></tr>");
|
||||||
addButton = $parent.find("tr:last a");
|
addButton = $parent.find("tr:last a");
|
||||||
} else {
|
} else {
|
||||||
// Otherwise, insert it immediately after the last form:
|
// Otherwise, insert it immediately after the last form:
|
||||||
$this.filter(":last").after('<div class="' + options.addCssClass + '"><a class="addlink" href="#">' + options.addText + "</a></div>");
|
$this.filter(":last").after('<div class="' + options.addCssClass + '"><a role="button" class="addlink" href="#">' + options.addText + "</a></div>");
|
||||||
addButton = $this.filter(":last").next().find("a");
|
addButton = $this.filter(":last").next().find("a");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -104,15 +104,15 @@
|
|||||||
if (row.is("tr")) {
|
if (row.is("tr")) {
|
||||||
// If the forms are laid out in table rows, insert
|
// If the forms are laid out in table rows, insert
|
||||||
// the remove button into the last table cell:
|
// the remove button into the last table cell:
|
||||||
row.children(":last").append('<div><a class="' + options.deleteCssClass + '" href="#">' + options.deleteText + "</a></div>");
|
row.children(":last").append('<div><a role="button" class="' + options.deleteCssClass + '" href="#">' + options.deleteText + "</a></div>");
|
||||||
} else if (row.is("ul") || row.is("ol")) {
|
} else if (row.is("ul") || row.is("ol")) {
|
||||||
// If they're laid out as an ordered/unordered list,
|
// If they're laid out as an ordered/unordered list,
|
||||||
// insert an <li> after the last list item:
|
// insert an <li> after the last list item:
|
||||||
row.append('<li><a class="' + options.deleteCssClass + '" href="#">' + options.deleteText + "</a></li>");
|
row.append('<li><a role="button" class="' + options.deleteCssClass + '" href="#">' + options.deleteText + "</a></li>");
|
||||||
} else {
|
} else {
|
||||||
// Otherwise, just insert the remove button as the
|
// Otherwise, just insert the remove button as the
|
||||||
// last child element of the form's container:
|
// last child element of the form's container:
|
||||||
row.children(":first").append('<span><a class="' + options.deleteCssClass + '" href="#">' + options.deleteText + "</a></span>");
|
row.children(":first").append('<span><a role="button" class="' + options.deleteCssClass + '" href="#">' + options.deleteText + "</a></span>");
|
||||||
}
|
}
|
||||||
// Add delete handler for each row.
|
// Add delete handler for each row.
|
||||||
row.find("a." + options.deleteCssClass).on('click', inlineDeleteHandler.bind(this));
|
row.find("a." + options.deleteCssClass).on('click', inlineDeleteHandler.bind(this));
|
||||||
|
@ -13,9 +13,9 @@
|
|||||||
{% if cl.result_count != cl.result_list|length %}
|
{% if cl.result_count != cl.result_list|length %}
|
||||||
<span class="all hidden">{{ selection_note_all }}</span>
|
<span class="all hidden">{{ selection_note_all }}</span>
|
||||||
<span class="question hidden">
|
<span class="question hidden">
|
||||||
<a href="#" title="{% translate "Click here to select the objects across all pages" %}">{% blocktranslate with cl.result_count as total_count %}Select all {{ total_count }} {{ module_name }}{% endblocktranslate %}</a>
|
<a role="button" href="#" title="{% translate "Click here to select the objects across all pages" %}">{% blocktranslate with cl.result_count as total_count %}Select all {{ total_count }} {{ module_name }}{% endblocktranslate %}</a>
|
||||||
</span>
|
</span>
|
||||||
<span class="clear hidden"><a href="#">{% translate "Clear selection" %}</a></span>
|
<span class="clear hidden"><a role="button" href="#">{% translate "Clear selection" %}</a></span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
{% block form_top %}
|
{% block form_top %}
|
||||||
{% if not is_popup %}
|
{% if not is_popup %}
|
||||||
<p>{% translate "After you've created a user, you’ll be able to edit more user options." %}</p>
|
<p>{% translate "After you’ve created a user, you’ll be able to edit more user options." %}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block extrahead %}
|
{% block extrahead %}
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
{% with name=fieldset.name|default:""|slugify %}
|
<fieldset class="module aligned {{ fieldset.classes }}"{% if fieldset.name %} aria-labelledby="{{ prefix }}-{{ id_prefix}}-{{ id_suffix }}-heading"{% endif %}>
|
||||||
<fieldset class="module aligned {{ fieldset.classes }}"{% if name %} aria-labelledby="{{ prefix }}-{{ id_prefix}}-{{ name }}-{{ id_suffix }}-heading"{% endif %}>
|
{% if fieldset.name %}
|
||||||
{% if name %}
|
|
||||||
{% if fieldset.is_collapsible %}<details><summary>{% endif %}
|
{% if fieldset.is_collapsible %}<details><summary>{% endif %}
|
||||||
<h{{ heading_level|default:2 }} id="{{ prefix }}-{{ id_prefix}}-{{ name }}-{{ id_suffix }}-heading" class="fieldset-heading">{{ fieldset.name }}</h{{ heading_level|default:2 }}>
|
<h{{ heading_level|default:2 }} id="{{ prefix }}-{{ id_prefix}}-{{ id_suffix }}-heading" class="fieldset-heading">{{ fieldset.name }}</h{{ heading_level|default:2 }}>
|
||||||
{% if fieldset.is_collapsible %}</summary>{% endif %}
|
{% if fieldset.is_collapsible %}</summary>{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if fieldset.description %}
|
{% if fieldset.description %}
|
||||||
@ -36,6 +35,5 @@
|
|||||||
{% if not line.fields|length == 1 %}</div>{% endif %}
|
{% if not line.fields|length == 1 %}</div>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if name and fieldset.is_collapsible %}</details>{% endif %}
|
{% if fieldset.name and fieldset.is_collapsible %}</details>{% endif %}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
{% endwith %}
|
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
<h1>{{ name }}</h1>
|
<h1>{{ name }}</h1>
|
||||||
|
|
||||||
<h2 class="subhead">{{ summary|striptags }}</h2>
|
<h2 class="subhead">{{ summary }}</h2>
|
||||||
|
|
||||||
{{ body }}
|
{{ body }}
|
||||||
|
|
||||||
|
@ -99,6 +99,21 @@ ROLES = {
|
|||||||
"tag": "%s/tags/#%s",
|
"tag": "%s/tags/#%s",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
explicit_title_re = re.compile(r"^(.+?)\s*(?<!\x00)<([^<]*?)>$", re.DOTALL)
|
||||||
|
|
||||||
|
|
||||||
|
def split_explicit_title(text):
|
||||||
|
"""
|
||||||
|
Split role content into title and target, if given.
|
||||||
|
|
||||||
|
From sphinx.util.nodes.split_explicit_title
|
||||||
|
See https://github.com/sphinx-doc/sphinx/blob/230ccf2/sphinx/util/nodes.py#L389
|
||||||
|
"""
|
||||||
|
match = explicit_title_re.match(text)
|
||||||
|
if match:
|
||||||
|
return True, match.group(1), match.group(2)
|
||||||
|
return False, text, text
|
||||||
|
|
||||||
|
|
||||||
def create_reference_role(rolename, urlbase):
|
def create_reference_role(rolename, urlbase):
|
||||||
# Views and template names are case-sensitive.
|
# Views and template names are case-sensitive.
|
||||||
@ -107,14 +122,15 @@ def create_reference_role(rolename, urlbase):
|
|||||||
def _role(name, rawtext, text, lineno, inliner, options=None, content=None):
|
def _role(name, rawtext, text, lineno, inliner, options=None, content=None):
|
||||||
if options is None:
|
if options is None:
|
||||||
options = {}
|
options = {}
|
||||||
|
_, title, target = split_explicit_title(text)
|
||||||
node = docutils.nodes.reference(
|
node = docutils.nodes.reference(
|
||||||
rawtext,
|
rawtext,
|
||||||
text,
|
title,
|
||||||
refuri=(
|
refuri=(
|
||||||
urlbase
|
urlbase
|
||||||
% (
|
% (
|
||||||
inliner.document.settings.link_base,
|
inliner.document.settings.link_base,
|
||||||
text if is_case_sensitive else text.lower(),
|
target if is_case_sensitive else target.lower(),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
**options,
|
**options,
|
||||||
@ -242,3 +258,7 @@ def remove_non_capturing_groups(pattern):
|
|||||||
final_pattern += pattern[prev_end:start]
|
final_pattern += pattern[prev_end:start]
|
||||||
prev_end = end
|
prev_end = end
|
||||||
return final_pattern + pattern[prev_end:]
|
return final_pattern + pattern[prev_end:]
|
||||||
|
|
||||||
|
|
||||||
|
def strip_p_tags(value):
|
||||||
|
return mark_safe(value.replace("<p>", "").replace("</p>", ""))
|
||||||
|
@ -13,7 +13,12 @@ from django.contrib.admindocs.utils import (
|
|||||||
replace_named_groups,
|
replace_named_groups,
|
||||||
replace_unnamed_groups,
|
replace_unnamed_groups,
|
||||||
)
|
)
|
||||||
from django.core.exceptions import ImproperlyConfigured, ViewDoesNotExist
|
from django.contrib.auth import get_permission_codename
|
||||||
|
from django.core.exceptions import (
|
||||||
|
ImproperlyConfigured,
|
||||||
|
PermissionDenied,
|
||||||
|
ViewDoesNotExist,
|
||||||
|
)
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
from django.template.engine import Engine
|
from django.template.engine import Engine
|
||||||
@ -30,7 +35,7 @@ from django.utils.inspect import (
|
|||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django.views.generic import TemplateView
|
from django.views.generic import TemplateView
|
||||||
|
|
||||||
from .utils import get_view_name
|
from .utils import get_view_name, strip_p_tags
|
||||||
|
|
||||||
# Exclude methods starting with these strings from documentation
|
# Exclude methods starting with these strings from documentation
|
||||||
MODEL_METHODS_EXCLUDE = ("_", "add_", "delete", "save", "set_")
|
MODEL_METHODS_EXCLUDE = ("_", "add_", "delete", "save", "set_")
|
||||||
@ -195,18 +200,31 @@ class ViewDetailView(BaseAdminDocsView):
|
|||||||
**{
|
**{
|
||||||
**kwargs,
|
**kwargs,
|
||||||
"name": view,
|
"name": view,
|
||||||
"summary": title,
|
"summary": strip_p_tags(title),
|
||||||
"body": body,
|
"body": body,
|
||||||
"meta": metadata,
|
"meta": metadata,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def user_has_model_view_permission(user, opts):
|
||||||
|
"""Based off ModelAdmin.has_view_permission."""
|
||||||
|
codename_view = get_permission_codename("view", opts)
|
||||||
|
codename_change = get_permission_codename("change", opts)
|
||||||
|
return user.has_perm("%s.%s" % (opts.app_label, codename_view)) or user.has_perm(
|
||||||
|
"%s.%s" % (opts.app_label, codename_change)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ModelIndexView(BaseAdminDocsView):
|
class ModelIndexView(BaseAdminDocsView):
|
||||||
template_name = "admin_doc/model_index.html"
|
template_name = "admin_doc/model_index.html"
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
m_list = [m._meta for m in apps.get_models()]
|
m_list = [
|
||||||
|
m._meta
|
||||||
|
for m in apps.get_models()
|
||||||
|
if user_has_model_view_permission(self.request.user, m._meta)
|
||||||
|
]
|
||||||
return super().get_context_data(**{**kwargs, "models": m_list})
|
return super().get_context_data(**{**kwargs, "models": m_list})
|
||||||
|
|
||||||
|
|
||||||
@ -228,6 +246,8 @@ class ModelDetailView(BaseAdminDocsView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
opts = model._meta
|
opts = model._meta
|
||||||
|
if not user_has_model_view_permission(self.request.user, opts):
|
||||||
|
raise PermissionDenied
|
||||||
|
|
||||||
title, body, metadata = utils.parse_docstring(model.__doc__)
|
title, body, metadata = utils.parse_docstring(model.__doc__)
|
||||||
title = title and utils.parse_rst(title, "model", _("model:") + model_name)
|
title = title and utils.parse_rst(title, "model", _("model:") + model_name)
|
||||||
@ -384,7 +404,7 @@ class ModelDetailView(BaseAdminDocsView):
|
|||||||
**{
|
**{
|
||||||
**kwargs,
|
**kwargs,
|
||||||
"name": opts.label,
|
"name": opts.label,
|
||||||
"summary": title,
|
"summary": strip_p_tags(title),
|
||||||
"description": body,
|
"description": body,
|
||||||
"fields": fields,
|
"fields": fields,
|
||||||
"methods": methods,
|
"methods": methods,
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
import inspect
|
import inspect
|
||||||
import re
|
import re
|
||||||
|
import warnings
|
||||||
|
|
||||||
from django.apps import apps as django_apps
|
from django.apps import apps as django_apps
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ImproperlyConfigured, PermissionDenied
|
from django.core.exceptions import ImproperlyConfigured, PermissionDenied
|
||||||
from django.middleware.csrf import rotate_token
|
from django.middleware.csrf import rotate_token
|
||||||
from django.utils.crypto import constant_time_compare
|
from django.utils.crypto import constant_time_compare
|
||||||
|
from django.utils.deprecation import RemovedInDjango61Warning
|
||||||
from django.utils.module_loading import import_string
|
from django.utils.module_loading import import_string
|
||||||
from django.views.decorators.debug import sensitive_variables
|
from django.views.decorators.debug import sensitive_variables
|
||||||
|
|
||||||
@ -154,9 +156,19 @@ def login(request, user, backend=None):
|
|||||||
have to reauthenticate on every request. Note that data set during
|
have to reauthenticate on every request. Note that data set during
|
||||||
the anonymous session is retained when the user logs in.
|
the anonymous session is retained when the user logs in.
|
||||||
"""
|
"""
|
||||||
|
# RemovedInDjango61Warning: When the deprecation ends, replace with:
|
||||||
|
# session_auth_hash = user.get_session_auth_hash()
|
||||||
session_auth_hash = ""
|
session_auth_hash = ""
|
||||||
|
# RemovedInDjango61Warning.
|
||||||
if user is None:
|
if user is None:
|
||||||
user = request.user
|
user = request.user
|
||||||
|
warnings.warn(
|
||||||
|
"Fallback to request.user when user is None will be removed.",
|
||||||
|
RemovedInDjango61Warning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
|
||||||
|
# RemovedInDjango61Warning.
|
||||||
if hasattr(user, "get_session_auth_hash"):
|
if hasattr(user, "get_session_auth_hash"):
|
||||||
session_auth_hash = user.get_session_auth_hash()
|
session_auth_hash = user.get_session_auth_hash()
|
||||||
|
|
||||||
@ -187,9 +199,18 @@ def login(request, user, backend=None):
|
|||||||
|
|
||||||
async def alogin(request, user, backend=None):
|
async def alogin(request, user, backend=None):
|
||||||
"""See login()."""
|
"""See login()."""
|
||||||
|
# RemovedInDjango61Warning: When the deprecation ends, replace with:
|
||||||
|
# session_auth_hash = user.get_session_auth_hash()
|
||||||
session_auth_hash = ""
|
session_auth_hash = ""
|
||||||
|
# RemovedInDjango61Warning.
|
||||||
if user is None:
|
if user is None:
|
||||||
|
warnings.warn(
|
||||||
|
"Fallback to request.user when user is None will be removed.",
|
||||||
|
RemovedInDjango61Warning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
user = await request.auser()
|
user = await request.auser()
|
||||||
|
# RemovedInDjango61Warning.
|
||||||
if hasattr(user, "get_session_auth_hash"):
|
if hasattr(user, "get_session_auth_hash"):
|
||||||
session_auth_hash = user.get_session_auth_hash()
|
session_auth_hash = user.get_session_auth_hash()
|
||||||
|
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import asyncio
|
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from urllib.parse import urlsplit
|
from urllib.parse import urlsplit
|
||||||
|
|
||||||
from asgiref.sync import async_to_sync, sync_to_async
|
from asgiref.sync import async_to_sync, iscoroutinefunction, sync_to_async
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth import REDIRECT_FIELD_NAME
|
from django.contrib.auth import REDIRECT_FIELD_NAME
|
||||||
@ -35,11 +34,11 @@ def user_passes_test(
|
|||||||
|
|
||||||
return redirect_to_login(path, resolved_login_url, redirect_field_name)
|
return redirect_to_login(path, resolved_login_url, redirect_field_name)
|
||||||
|
|
||||||
if asyncio.iscoroutinefunction(view_func):
|
if iscoroutinefunction(view_func):
|
||||||
|
|
||||||
async def _view_wrapper(request, *args, **kwargs):
|
async def _view_wrapper(request, *args, **kwargs):
|
||||||
auser = await request.auser()
|
auser = await request.auser()
|
||||||
if asyncio.iscoroutinefunction(test_func):
|
if iscoroutinefunction(test_func):
|
||||||
test_pass = await test_func(auser)
|
test_pass = await test_func(auser)
|
||||||
else:
|
else:
|
||||||
test_pass = await sync_to_async(test_func)(auser)
|
test_pass = await sync_to_async(test_func)(auser)
|
||||||
@ -51,7 +50,7 @@ def user_passes_test(
|
|||||||
else:
|
else:
|
||||||
|
|
||||||
def _view_wrapper(request, *args, **kwargs):
|
def _view_wrapper(request, *args, **kwargs):
|
||||||
if asyncio.iscoroutinefunction(test_func):
|
if iscoroutinefunction(test_func):
|
||||||
test_pass = async_to_sync(test_func)(request.user)
|
test_pass = async_to_sync(test_func)(request.user)
|
||||||
else:
|
else:
|
||||||
test_pass = test_func(request.user)
|
test_pass = test_func(request.user)
|
||||||
@ -107,7 +106,7 @@ def permission_required(perm, login_url=None, raise_exception=False):
|
|||||||
perms = perm
|
perms = perm
|
||||||
|
|
||||||
def decorator(view_func):
|
def decorator(view_func):
|
||||||
if asyncio.iscoroutinefunction(view_func):
|
if iscoroutinefunction(view_func):
|
||||||
|
|
||||||
async def check_perms(user):
|
async def check_perms(user):
|
||||||
# First check if the user has the permission (even anon users).
|
# First check if the user has the permission (even anon users).
|
||||||
|
@ -15,6 +15,7 @@ from django.utils.http import urlsafe_base64_encode
|
|||||||
from django.utils.text import capfirst
|
from django.utils.text import capfirst
|
||||||
from django.utils.translation import gettext
|
from django.utils.translation import gettext
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.views.decorators.debug import sensitive_variables
|
||||||
|
|
||||||
UserModel = get_user_model()
|
UserModel = get_user_model()
|
||||||
logger = logging.getLogger("django.contrib.auth")
|
logger = logging.getLogger("django.contrib.auth")
|
||||||
@ -122,6 +123,7 @@ class SetPasswordMixin:
|
|||||||
)
|
)
|
||||||
return password1, password2
|
return password1, password2
|
||||||
|
|
||||||
|
@sensitive_variables("password1", "password2")
|
||||||
def validate_passwords(
|
def validate_passwords(
|
||||||
self,
|
self,
|
||||||
password1_field_name="password1",
|
password1_field_name="password1",
|
||||||
@ -151,6 +153,7 @@ class SetPasswordMixin:
|
|||||||
)
|
)
|
||||||
self.add_error(password2_field_name, error)
|
self.add_error(password2_field_name, error)
|
||||||
|
|
||||||
|
@sensitive_variables("password")
|
||||||
def validate_password_for_user(self, user, password_field_name="password2"):
|
def validate_password_for_user(self, user, password_field_name="password2"):
|
||||||
password = self.cleaned_data.get(password_field_name)
|
password = self.cleaned_data.get(password_field_name)
|
||||||
if password:
|
if password:
|
||||||
@ -348,6 +351,7 @@ class AuthenticationForm(forms.Form):
|
|||||||
if self.fields["username"].label is None:
|
if self.fields["username"].label is None:
|
||||||
self.fields["username"].label = capfirst(self.username_field.verbose_name)
|
self.fields["username"].label = capfirst(self.username_field.verbose_name)
|
||||||
|
|
||||||
|
@sensitive_variables()
|
||||||
def clean(self):
|
def clean(self):
|
||||||
username = self.cleaned_data.get("username")
|
username = self.cleaned_data.get("username")
|
||||||
password = self.cleaned_data.get("password")
|
password = self.cleaned_data.get("password")
|
||||||
@ -539,6 +543,7 @@ class PasswordChangeForm(SetPasswordForm):
|
|||||||
|
|
||||||
field_order = ["old_password", "new_password1", "new_password2"]
|
field_order = ["old_password", "new_password1", "new_password2"]
|
||||||
|
|
||||||
|
@sensitive_variables("old_password")
|
||||||
def clean_old_password(self):
|
def clean_old_password(self):
|
||||||
"""
|
"""
|
||||||
Validate that the old_password field is correct.
|
Validate that the old_password field is correct.
|
||||||
|
@ -115,10 +115,12 @@ def get_system_username():
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
result = getpass.getuser()
|
result = getpass.getuser()
|
||||||
except (ImportError, KeyError):
|
except (ImportError, KeyError, OSError):
|
||||||
# KeyError will be raised by os.getpwuid() (called by getuser())
|
# TODO: Drop ImportError and KeyError when dropping support for PY312.
|
||||||
# if there is no corresponding entry in the /etc/passwd file
|
# KeyError (Python <3.13) or OSError (Python 3.13+) will be raised by
|
||||||
# (a very restricted chroot environment, for example).
|
# os.getpwuid() (called by getuser()) if there is no corresponding
|
||||||
|
# entry in the /etc/passwd file (for example, in a very restricted
|
||||||
|
# chroot environment).
|
||||||
return ""
|
return ""
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
@ -174,11 +174,15 @@ class UserManager(BaseUserManager):
|
|||||||
extra_fields.setdefault("is_superuser", False)
|
extra_fields.setdefault("is_superuser", False)
|
||||||
return self._create_user(username, email, password, **extra_fields)
|
return self._create_user(username, email, password, **extra_fields)
|
||||||
|
|
||||||
|
create_user.alters_data = True
|
||||||
|
|
||||||
async def acreate_user(self, username, email=None, password=None, **extra_fields):
|
async def acreate_user(self, username, email=None, password=None, **extra_fields):
|
||||||
extra_fields.setdefault("is_staff", False)
|
extra_fields.setdefault("is_staff", False)
|
||||||
extra_fields.setdefault("is_superuser", False)
|
extra_fields.setdefault("is_superuser", False)
|
||||||
return await self._acreate_user(username, email, password, **extra_fields)
|
return await self._acreate_user(username, email, password, **extra_fields)
|
||||||
|
|
||||||
|
acreate_user.alters_data = True
|
||||||
|
|
||||||
def create_superuser(self, username, email=None, password=None, **extra_fields):
|
def create_superuser(self, username, email=None, password=None, **extra_fields):
|
||||||
extra_fields.setdefault("is_staff", True)
|
extra_fields.setdefault("is_staff", True)
|
||||||
extra_fields.setdefault("is_superuser", True)
|
extra_fields.setdefault("is_superuser", True)
|
||||||
@ -190,6 +194,8 @@ class UserManager(BaseUserManager):
|
|||||||
|
|
||||||
return self._create_user(username, email, password, **extra_fields)
|
return self._create_user(username, email, password, **extra_fields)
|
||||||
|
|
||||||
|
create_superuser.alters_data = True
|
||||||
|
|
||||||
async def acreate_superuser(
|
async def acreate_superuser(
|
||||||
self, username, email=None, password=None, **extra_fields
|
self, username, email=None, password=None, **extra_fields
|
||||||
):
|
):
|
||||||
@ -203,6 +209,8 @@ class UserManager(BaseUserManager):
|
|||||||
|
|
||||||
return await self._acreate_user(username, email, password, **extra_fields)
|
return await self._acreate_user(username, email, password, **extra_fields)
|
||||||
|
|
||||||
|
acreate_superuser.alters_data = True
|
||||||
|
|
||||||
def with_perm(
|
def with_perm(
|
||||||
self, perm, is_active=True, include_superusers=True, backend=None, obj=None
|
self, perm, is_active=True, include_superusers=True, backend=None, obj=None
|
||||||
):
|
):
|
||||||
|
@ -106,17 +106,16 @@ class MinimumLengthValidator:
|
|||||||
|
|
||||||
def validate(self, password, user=None):
|
def validate(self, password, user=None):
|
||||||
if len(password) < self.min_length:
|
if len(password) < self.min_length:
|
||||||
raise ValidationError(
|
raise ValidationError(self.get_error_message(), code="password_too_short")
|
||||||
ngettext(
|
|
||||||
"This password is too short. It must contain at least "
|
def get_error_message(self):
|
||||||
"%(min_length)d character.",
|
return ngettext(
|
||||||
"This password is too short. It must contain at least "
|
"This password is too short. It must contain at least %d character."
|
||||||
"%(min_length)d characters.",
|
% self.min_length,
|
||||||
self.min_length,
|
"This password is too short. It must contain at least %d characters."
|
||||||
),
|
% self.min_length,
|
||||||
code="password_too_short",
|
self.min_length,
|
||||||
params={"min_length": self.min_length},
|
)
|
||||||
)
|
|
||||||
|
|
||||||
def get_help_text(self):
|
def get_help_text(self):
|
||||||
return ngettext(
|
return ngettext(
|
||||||
@ -203,11 +202,14 @@ class UserAttributeSimilarityValidator:
|
|||||||
except FieldDoesNotExist:
|
except FieldDoesNotExist:
|
||||||
verbose_name = attribute_name
|
verbose_name = attribute_name
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
_("The password is too similar to the %(verbose_name)s."),
|
self.get_error_message(),
|
||||||
code="password_too_similar",
|
code="password_too_similar",
|
||||||
params={"verbose_name": verbose_name},
|
params={"verbose_name": verbose_name},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_error_message(self):
|
||||||
|
return _("The password is too similar to the %(verbose_name)s.")
|
||||||
|
|
||||||
def get_help_text(self):
|
def get_help_text(self):
|
||||||
return _(
|
return _(
|
||||||
"Your password can’t be too similar to your other personal information."
|
"Your password can’t be too similar to your other personal information."
|
||||||
@ -242,10 +244,13 @@ class CommonPasswordValidator:
|
|||||||
def validate(self, password, user=None):
|
def validate(self, password, user=None):
|
||||||
if password.lower().strip() in self.passwords:
|
if password.lower().strip() in self.passwords:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
_("This password is too common."),
|
self.get_error_message(),
|
||||||
code="password_too_common",
|
code="password_too_common",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_error_message(self):
|
||||||
|
return _("This password is too common.")
|
||||||
|
|
||||||
def get_help_text(self):
|
def get_help_text(self):
|
||||||
return _("Your password can’t be a commonly used password.")
|
return _("Your password can’t be a commonly used password.")
|
||||||
|
|
||||||
@ -258,9 +263,12 @@ class NumericPasswordValidator:
|
|||||||
def validate(self, password, user=None):
|
def validate(self, password, user=None):
|
||||||
if password.isdigit():
|
if password.isdigit():
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
_("This password is entirely numeric."),
|
self.get_error_message(),
|
||||||
code="password_entirely_numeric",
|
code="password_entirely_numeric",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_error_message(self):
|
||||||
|
return _("This password is entirely numeric.")
|
||||||
|
|
||||||
def get_help_text(self):
|
def get_help_text(self):
|
||||||
return _("Your password can’t be entirely numeric.")
|
return _("Your password can’t be entirely numeric.")
|
||||||
|
@ -45,6 +45,7 @@ class MySQLOperations(BaseSpatialOperations, DatabaseOperations):
|
|||||||
"bboverlaps": SpatialOperator(func="MBROverlaps"), # ...
|
"bboverlaps": SpatialOperator(func="MBROverlaps"), # ...
|
||||||
"contained": SpatialOperator(func="MBRWithin"), # ...
|
"contained": SpatialOperator(func="MBRWithin"), # ...
|
||||||
"contains": SpatialOperator(func="ST_Contains"),
|
"contains": SpatialOperator(func="ST_Contains"),
|
||||||
|
"coveredby": SpatialOperator(func="MBRCoveredBy"),
|
||||||
"crosses": SpatialOperator(func="ST_Crosses"),
|
"crosses": SpatialOperator(func="ST_Crosses"),
|
||||||
"disjoint": SpatialOperator(func="ST_Disjoint"),
|
"disjoint": SpatialOperator(func="ST_Disjoint"),
|
||||||
"equals": SpatialOperator(func="ST_Equals"),
|
"equals": SpatialOperator(func="ST_Equals"),
|
||||||
@ -57,6 +58,10 @@ class MySQLOperations(BaseSpatialOperations, DatabaseOperations):
|
|||||||
}
|
}
|
||||||
if self.connection.mysql_is_mariadb:
|
if self.connection.mysql_is_mariadb:
|
||||||
operators["relate"] = SpatialOperator(func="ST_Relate")
|
operators["relate"] = SpatialOperator(func="ST_Relate")
|
||||||
|
if self.connection.mysql_version < (11, 7):
|
||||||
|
del operators["coveredby"]
|
||||||
|
else:
|
||||||
|
operators["covers"] = SpatialOperator(func="MBRCovers")
|
||||||
return operators
|
return operators
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
@ -68,7 +73,10 @@ class MySQLOperations(BaseSpatialOperations, DatabaseOperations):
|
|||||||
models.Union,
|
models.Union,
|
||||||
]
|
]
|
||||||
is_mariadb = self.connection.mysql_is_mariadb
|
is_mariadb = self.connection.mysql_is_mariadb
|
||||||
if is_mariadb or self.connection.mysql_version < (8, 0, 24):
|
if is_mariadb:
|
||||||
|
if self.connection.mysql_version < (11, 7):
|
||||||
|
disallowed_aggregates.insert(0, models.Collect)
|
||||||
|
elif self.connection.mysql_version < (8, 0, 24):
|
||||||
disallowed_aggregates.insert(0, models.Collect)
|
disallowed_aggregates.insert(0, models.Collect)
|
||||||
return tuple(disallowed_aggregates)
|
return tuple(disallowed_aggregates)
|
||||||
|
|
||||||
@ -102,7 +110,8 @@ class MySQLOperations(BaseSpatialOperations, DatabaseOperations):
|
|||||||
}
|
}
|
||||||
if self.connection.mysql_is_mariadb:
|
if self.connection.mysql_is_mariadb:
|
||||||
unsupported.remove("PointOnSurface")
|
unsupported.remove("PointOnSurface")
|
||||||
unsupported.update({"GeoHash", "IsValid"})
|
if self.connection.mysql_version < (11, 7):
|
||||||
|
unsupported.update({"GeoHash", "IsValid"})
|
||||||
return unsupported
|
return unsupported
|
||||||
|
|
||||||
def geo_db_type(self, f):
|
def geo_db_type(self, f):
|
||||||
|
@ -64,6 +64,7 @@ class OGRGeometry(GDALBase):
|
|||||||
"""Encapsulate an OGR geometry."""
|
"""Encapsulate an OGR geometry."""
|
||||||
|
|
||||||
destructor = capi.destroy_geom
|
destructor = capi.destroy_geom
|
||||||
|
geos_support = True
|
||||||
|
|
||||||
def __init__(self, geom_input, srs=None):
|
def __init__(self, geom_input, srs=None):
|
||||||
"""Initialize Geometry on either WKT or an OGR pointer as input."""
|
"""Initialize Geometry on either WKT or an OGR pointer as input."""
|
||||||
@ -304,6 +305,19 @@ class OGRGeometry(GDALBase):
|
|||||||
f"Input to 'set_measured' must be a boolean, got '{value!r}'."
|
f"Input to 'set_measured' must be a boolean, got '{value!r}'."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_curve(self):
|
||||||
|
"""Return True if the geometry is or has curve geometry."""
|
||||||
|
return capi.has_curve_geom(self.ptr, 0)
|
||||||
|
|
||||||
|
def get_linear_geometry(self):
|
||||||
|
"""Return a linear version of this geometry."""
|
||||||
|
return OGRGeometry(capi.get_linear_geom(self.ptr, 0, None))
|
||||||
|
|
||||||
|
def get_curve_geometry(self):
|
||||||
|
"""Return a curve version of this geometry."""
|
||||||
|
return OGRGeometry(capi.get_curve_geom(self.ptr, None))
|
||||||
|
|
||||||
# #### SpatialReference-related Properties ####
|
# #### SpatialReference-related Properties ####
|
||||||
|
|
||||||
# The SRS property
|
# The SRS property
|
||||||
@ -360,9 +374,14 @@ class OGRGeometry(GDALBase):
|
|||||||
@property
|
@property
|
||||||
def geos(self):
|
def geos(self):
|
||||||
"Return a GEOSGeometry object from this OGRGeometry."
|
"Return a GEOSGeometry object from this OGRGeometry."
|
||||||
from django.contrib.gis.geos import GEOSGeometry
|
if self.geos_support:
|
||||||
|
from django.contrib.gis.geos import GEOSGeometry
|
||||||
|
|
||||||
return GEOSGeometry(self._geos_ptr(), self.srid)
|
return GEOSGeometry(self._geos_ptr(), self.srid)
|
||||||
|
else:
|
||||||
|
from django.contrib.gis.geos import GEOSException
|
||||||
|
|
||||||
|
raise GEOSException(f"GEOS does not support {self.__class__.__qualname__}.")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def gml(self):
|
def gml(self):
|
||||||
@ -727,6 +746,18 @@ class Polygon(OGRGeometry):
|
|||||||
return sum(self[i].point_count for i in range(self.geom_count))
|
return sum(self[i].point_count for i in range(self.geom_count))
|
||||||
|
|
||||||
|
|
||||||
|
class CircularString(LineString):
|
||||||
|
geos_support = False
|
||||||
|
|
||||||
|
|
||||||
|
class CurvePolygon(Polygon):
|
||||||
|
geos_support = False
|
||||||
|
|
||||||
|
|
||||||
|
class CompoundCurve(OGRGeometry):
|
||||||
|
geos_support = False
|
||||||
|
|
||||||
|
|
||||||
# Geometry Collection base class.
|
# Geometry Collection base class.
|
||||||
class GeometryCollection(OGRGeometry):
|
class GeometryCollection(OGRGeometry):
|
||||||
"The Geometry Collection class."
|
"The Geometry Collection class."
|
||||||
@ -788,6 +819,14 @@ class MultiPolygon(GeometryCollection):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class MultiSurface(GeometryCollection):
|
||||||
|
geos_support = False
|
||||||
|
|
||||||
|
|
||||||
|
class MultiCurve(GeometryCollection):
|
||||||
|
geos_support = False
|
||||||
|
|
||||||
|
|
||||||
# Class mapping dictionary (using the OGRwkbGeometryType as the key)
|
# Class mapping dictionary (using the OGRwkbGeometryType as the key)
|
||||||
GEO_CLASSES = {
|
GEO_CLASSES = {
|
||||||
1: Point,
|
1: Point,
|
||||||
@ -797,7 +836,17 @@ GEO_CLASSES = {
|
|||||||
5: MultiLineString,
|
5: MultiLineString,
|
||||||
6: MultiPolygon,
|
6: MultiPolygon,
|
||||||
7: GeometryCollection,
|
7: GeometryCollection,
|
||||||
|
8: CircularString,
|
||||||
|
9: CompoundCurve,
|
||||||
|
10: CurvePolygon,
|
||||||
|
11: MultiCurve,
|
||||||
|
12: MultiSurface,
|
||||||
101: LinearRing,
|
101: LinearRing,
|
||||||
|
1008: CircularString, # CIRCULARSTRING Z
|
||||||
|
1009: CompoundCurve, # COMPOUNDCURVE Z
|
||||||
|
1010: CurvePolygon, # CURVEPOLYGON Z
|
||||||
|
1011: MultiCurve, # MULTICURVE Z
|
||||||
|
1012: MultiSurface, # MULTICURVE Z
|
||||||
2001: Point, # POINT M
|
2001: Point, # POINT M
|
||||||
2002: LineString, # LINESTRING M
|
2002: LineString, # LINESTRING M
|
||||||
2003: Polygon, # POLYGON M
|
2003: Polygon, # POLYGON M
|
||||||
@ -805,6 +854,11 @@ GEO_CLASSES = {
|
|||||||
2005: MultiLineString, # MULTILINESTRING M
|
2005: MultiLineString, # MULTILINESTRING M
|
||||||
2006: MultiPolygon, # MULTIPOLYGON M
|
2006: MultiPolygon, # MULTIPOLYGON M
|
||||||
2007: GeometryCollection, # GEOMETRYCOLLECTION M
|
2007: GeometryCollection, # GEOMETRYCOLLECTION M
|
||||||
|
2008: CircularString, # CIRCULARSTRING M
|
||||||
|
2009: CompoundCurve, # COMPOUNDCURVE M
|
||||||
|
2010: CurvePolygon, # CURVEPOLYGON M
|
||||||
|
2011: MultiCurve, # MULTICURVE M
|
||||||
|
2012: MultiSurface, # MULTICURVE M
|
||||||
3001: Point, # POINT ZM
|
3001: Point, # POINT ZM
|
||||||
3002: LineString, # LINESTRING ZM
|
3002: LineString, # LINESTRING ZM
|
||||||
3003: Polygon, # POLYGON ZM
|
3003: Polygon, # POLYGON ZM
|
||||||
@ -812,6 +866,11 @@ GEO_CLASSES = {
|
|||||||
3005: MultiLineString, # MULTILINESTRING ZM
|
3005: MultiLineString, # MULTILINESTRING ZM
|
||||||
3006: MultiPolygon, # MULTIPOLYGON ZM
|
3006: MultiPolygon, # MULTIPOLYGON ZM
|
||||||
3007: GeometryCollection, # GEOMETRYCOLLECTION ZM
|
3007: GeometryCollection, # GEOMETRYCOLLECTION ZM
|
||||||
|
3008: CircularString, # CIRCULARSTRING ZM
|
||||||
|
3009: CompoundCurve, # COMPOUNDCURVE ZM
|
||||||
|
3010: CurvePolygon, # CURVEPOLYGON ZM
|
||||||
|
3011: MultiCurve, # MULTICURVE ZM
|
||||||
|
3012: MultiSurface, # MULTISURFACE ZM
|
||||||
1 + OGRGeomType.wkb25bit: Point, # POINT Z
|
1 + OGRGeomType.wkb25bit: Point, # POINT Z
|
||||||
2 + OGRGeomType.wkb25bit: LineString, # LINESTRING Z
|
2 + OGRGeomType.wkb25bit: LineString, # LINESTRING Z
|
||||||
3 + OGRGeomType.wkb25bit: Polygon, # POLYGON Z
|
3 + OGRGeomType.wkb25bit: Polygon, # POLYGON Z
|
||||||
|
@ -22,6 +22,7 @@ if lib_path:
|
|||||||
elif os.name == "nt":
|
elif os.name == "nt":
|
||||||
# Windows NT shared libraries
|
# Windows NT shared libraries
|
||||||
lib_names = [
|
lib_names = [
|
||||||
|
"gdal309",
|
||||||
"gdal308",
|
"gdal308",
|
||||||
"gdal307",
|
"gdal307",
|
||||||
"gdal306",
|
"gdal306",
|
||||||
@ -36,6 +37,7 @@ elif os.name == "posix":
|
|||||||
lib_names = [
|
lib_names = [
|
||||||
"gdal",
|
"gdal",
|
||||||
"GDAL",
|
"GDAL",
|
||||||
|
"gdal3.9.0",
|
||||||
"gdal3.8.0",
|
"gdal3.8.0",
|
||||||
"gdal3.7.0",
|
"gdal3.7.0",
|
||||||
"gdal3.6.0",
|
"gdal3.6.0",
|
||||||
|
@ -85,6 +85,13 @@ is_3d = bool_output(lgdal.OGR_G_Is3D, [c_void_p])
|
|||||||
set_3d = void_output(lgdal.OGR_G_Set3D, [c_void_p, c_int], errcheck=False)
|
set_3d = void_output(lgdal.OGR_G_Set3D, [c_void_p, c_int], errcheck=False)
|
||||||
is_measured = bool_output(lgdal.OGR_G_IsMeasured, [c_void_p])
|
is_measured = bool_output(lgdal.OGR_G_IsMeasured, [c_void_p])
|
||||||
set_measured = void_output(lgdal.OGR_G_SetMeasured, [c_void_p, c_int], errcheck=False)
|
set_measured = void_output(lgdal.OGR_G_SetMeasured, [c_void_p, c_int], errcheck=False)
|
||||||
|
has_curve_geom = bool_output(lgdal.OGR_G_HasCurveGeometry, [c_void_p, c_int])
|
||||||
|
get_linear_geom = geom_output(
|
||||||
|
lgdal.OGR_G_GetLinearGeometry, [c_void_p, c_double, POINTER(c_char_p)]
|
||||||
|
)
|
||||||
|
get_curve_geom = geom_output(
|
||||||
|
lgdal.OGR_G_GetCurveGeometry, [c_void_p, POINTER(c_char_p)]
|
||||||
|
)
|
||||||
|
|
||||||
# Geometry modification routines.
|
# Geometry modification routines.
|
||||||
add_geom = void_output(lgdal.OGR_G_AddGeometry, [c_void_p, c_void_p])
|
add_geom = void_output(lgdal.OGR_G_AddGeometry, [c_void_p, c_void_p])
|
||||||
|
@ -34,6 +34,18 @@ else:
|
|||||||
__all__ += ["GeoIP2", "GeoIP2Exception"]
|
__all__ += ["GeoIP2", "GeoIP2Exception"]
|
||||||
|
|
||||||
|
|
||||||
|
# These are the values stored in the `database_type` field of the metadata.
|
||||||
|
# See https://maxmind.github.io/MaxMind-DB/#database_type for details.
|
||||||
|
SUPPORTED_DATABASE_TYPES = {
|
||||||
|
"DBIP-City-Lite",
|
||||||
|
"DBIP-Country-Lite",
|
||||||
|
"GeoIP2-City",
|
||||||
|
"GeoIP2-Country",
|
||||||
|
"GeoLite2-City",
|
||||||
|
"GeoLite2-Country",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class GeoIP2Exception(Exception):
|
class GeoIP2Exception(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -106,7 +118,7 @@ class GeoIP2:
|
|||||||
)
|
)
|
||||||
|
|
||||||
database_type = self._metadata.database_type
|
database_type = self._metadata.database_type
|
||||||
if not database_type.endswith(("City", "Country")):
|
if database_type not in SUPPORTED_DATABASE_TYPES:
|
||||||
raise GeoIP2Exception(f"Unable to handle database edition: {database_type}")
|
raise GeoIP2Exception(f"Unable to handle database edition: {database_type}")
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
@ -123,6 +135,14 @@ class GeoIP2:
|
|||||||
def _metadata(self):
|
def _metadata(self):
|
||||||
return self._reader.metadata()
|
return self._reader.metadata()
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def is_city(self):
|
||||||
|
return "City" in self._metadata.database_type
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def is_country(self):
|
||||||
|
return "Country" in self._metadata.database_type
|
||||||
|
|
||||||
def _query(self, query, *, require_city=False):
|
def _query(self, query, *, require_city=False):
|
||||||
if not isinstance(query, (str, ipaddress.IPv4Address, ipaddress.IPv6Address)):
|
if not isinstance(query, (str, ipaddress.IPv4Address, ipaddress.IPv6Address)):
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
@ -130,9 +150,7 @@ class GeoIP2:
|
|||||||
"IPv6Address, not type %s" % type(query).__name__,
|
"IPv6Address, not type %s" % type(query).__name__,
|
||||||
)
|
)
|
||||||
|
|
||||||
is_city = self._metadata.database_type.endswith("City")
|
if require_city and not self.is_city:
|
||||||
|
|
||||||
if require_city and not is_city:
|
|
||||||
raise GeoIP2Exception(f"Invalid GeoIP city data file: {self._path}")
|
raise GeoIP2Exception(f"Invalid GeoIP city data file: {self._path}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -141,7 +159,7 @@ class GeoIP2:
|
|||||||
# GeoIP2 only takes IP addresses, so try to resolve a hostname.
|
# GeoIP2 only takes IP addresses, so try to resolve a hostname.
|
||||||
query = socket.gethostbyname(query)
|
query = socket.gethostbyname(query)
|
||||||
|
|
||||||
function = self._reader.city if is_city else self._reader.country
|
function = self._reader.city if self.is_city else self._reader.country
|
||||||
return function(query)
|
return function(query)
|
||||||
|
|
||||||
def city(self, query):
|
def city(self, query):
|
||||||
|
@ -279,14 +279,14 @@ class Command(BaseCommand):
|
|||||||
try:
|
try:
|
||||||
# When was the target file modified last time?
|
# When was the target file modified last time?
|
||||||
target_last_modified = self.storage.get_modified_time(prefixed_path)
|
target_last_modified = self.storage.get_modified_time(prefixed_path)
|
||||||
except (OSError, NotImplementedError, AttributeError):
|
except (OSError, NotImplementedError):
|
||||||
# The storage doesn't support get_modified_time() or failed
|
# The storage doesn't support get_modified_time() or failed
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
# When was the source file modified last time?
|
# When was the source file modified last time?
|
||||||
source_last_modified = source_storage.get_modified_time(path)
|
source_last_modified = source_storage.get_modified_time(path)
|
||||||
except (OSError, NotImplementedError, AttributeError):
|
except (OSError, NotImplementedError):
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
# The full path of the target file
|
# The full path of the target file
|
||||||
|
2
django/core/cache/backends/filebased.py
vendored
2
django/core/cache/backends/filebased.py
vendored
@ -166,5 +166,5 @@ class FileBasedCache(BaseCache):
|
|||||||
"""
|
"""
|
||||||
return [
|
return [
|
||||||
os.path.join(self._dir, fname)
|
os.path.join(self._dir, fname)
|
||||||
for fname in glob.glob1(self._dir, "*%s" % self.cache_suffix)
|
for fname in glob.glob(f"*{self.cache_suffix}", root_dir=self._dir)
|
||||||
]
|
]
|
||||||
|
@ -16,6 +16,7 @@ from .registry import Tags, register, run_checks, tag_exists
|
|||||||
# Import these to force registration of checks
|
# Import these to force registration of checks
|
||||||
import django.core.checks.async_checks # NOQA isort:skip
|
import django.core.checks.async_checks # NOQA isort:skip
|
||||||
import django.core.checks.caches # NOQA isort:skip
|
import django.core.checks.caches # NOQA isort:skip
|
||||||
|
import django.core.checks.commands # NOQA isort:skip
|
||||||
import django.core.checks.compatibility.django_4_0 # NOQA isort:skip
|
import django.core.checks.compatibility.django_4_0 # NOQA isort:skip
|
||||||
import django.core.checks.database # NOQA isort:skip
|
import django.core.checks.database # NOQA isort:skip
|
||||||
import django.core.checks.files # NOQA isort:skip
|
import django.core.checks.files # NOQA isort:skip
|
||||||
|
28
django/core/checks/commands.py
Normal file
28
django/core/checks/commands.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
from django.core.checks import Error, Tags, register
|
||||||
|
|
||||||
|
|
||||||
|
@register(Tags.commands)
|
||||||
|
def migrate_and_makemigrations_autodetector(**kwargs):
|
||||||
|
from django.core.management import get_commands, load_command_class
|
||||||
|
|
||||||
|
commands = get_commands()
|
||||||
|
|
||||||
|
make_migrations = load_command_class(commands["makemigrations"], "makemigrations")
|
||||||
|
migrate = load_command_class(commands["migrate"], "migrate")
|
||||||
|
|
||||||
|
if make_migrations.autodetector is not migrate.autodetector:
|
||||||
|
return [
|
||||||
|
Error(
|
||||||
|
"The migrate and makemigrations commands must have the same "
|
||||||
|
"autodetector.",
|
||||||
|
hint=(
|
||||||
|
f"makemigrations.Command.autodetector is "
|
||||||
|
f"{make_migrations.autodetector.__name__}, but "
|
||||||
|
f"migrate.Command.autodetector is "
|
||||||
|
f"{migrate.autodetector.__name__}."
|
||||||
|
),
|
||||||
|
id="commands.E001",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
return []
|
@ -12,6 +12,7 @@ class Tags:
|
|||||||
admin = "admin"
|
admin = "admin"
|
||||||
async_support = "async_support"
|
async_support = "async_support"
|
||||||
caches = "caches"
|
caches = "caches"
|
||||||
|
commands = "commands"
|
||||||
compatibility = "compatibility"
|
compatibility = "compatibility"
|
||||||
database = "database"
|
database = "database"
|
||||||
files = "files"
|
files = "files"
|
||||||
|
@ -24,6 +24,7 @@ from django.db.migrations.writer import MigrationWriter
|
|||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
|
autodetector = MigrationAutodetector
|
||||||
help = "Creates new migration(s) for apps."
|
help = "Creates new migration(s) for apps."
|
||||||
|
|
||||||
def add_arguments(self, parser):
|
def add_arguments(self, parser):
|
||||||
@ -209,7 +210,7 @@ class Command(BaseCommand):
|
|||||||
log=self.log,
|
log=self.log,
|
||||||
)
|
)
|
||||||
# Set up autodetector
|
# Set up autodetector
|
||||||
autodetector = MigrationAutodetector(
|
autodetector = self.autodetector(
|
||||||
loader.project_state(),
|
loader.project_state(),
|
||||||
ProjectState.from_apps(apps),
|
ProjectState.from_apps(apps),
|
||||||
questioner,
|
questioner,
|
||||||
@ -461,7 +462,7 @@ class Command(BaseCommand):
|
|||||||
# If they still want to merge it, then write out an empty
|
# If they still want to merge it, then write out an empty
|
||||||
# file depending on the migrations needing merging.
|
# file depending on the migrations needing merging.
|
||||||
numbers = [
|
numbers = [
|
||||||
MigrationAutodetector.parse_number(migration.name)
|
self.autodetector.parse_number(migration.name)
|
||||||
for migration in merge_migrations
|
for migration in merge_migrations
|
||||||
]
|
]
|
||||||
try:
|
try:
|
||||||
|
@ -15,6 +15,7 @@ from django.utils.text import Truncator
|
|||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
|
autodetector = MigrationAutodetector
|
||||||
help = (
|
help = (
|
||||||
"Updates database schema. Manages both apps with migrations and those without."
|
"Updates database schema. Manages both apps with migrations and those without."
|
||||||
)
|
)
|
||||||
@ -329,7 +330,7 @@ class Command(BaseCommand):
|
|||||||
self.stdout.write(" No migrations to apply.")
|
self.stdout.write(" No migrations to apply.")
|
||||||
# If there's changes that aren't in migrations yet, tell them
|
# If there's changes that aren't in migrations yet, tell them
|
||||||
# how to fix it.
|
# how to fix it.
|
||||||
autodetector = MigrationAutodetector(
|
autodetector = self.autodetector(
|
||||||
executor.loader.project_state(),
|
executor.loader.project_state(),
|
||||||
ProjectState.from_apps(apps),
|
ProjectState.from_apps(apps),
|
||||||
)
|
)
|
||||||
|
@ -5,6 +5,7 @@ Requires PyYaml (https://pyyaml.org/), but that's checked for in __init__.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import collections
|
import collections
|
||||||
|
import datetime
|
||||||
import decimal
|
import decimal
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
@ -12,7 +13,6 @@ import yaml
|
|||||||
from django.core.serializers.base import DeserializationError
|
from django.core.serializers.base import DeserializationError
|
||||||
from django.core.serializers.python import Deserializer as PythonDeserializer
|
from django.core.serializers.python import Deserializer as PythonDeserializer
|
||||||
from django.core.serializers.python import Serializer as PythonSerializer
|
from django.core.serializers.python import Serializer as PythonSerializer
|
||||||
from django.db import models
|
|
||||||
|
|
||||||
# Use the C (faster) implementation if possible
|
# Use the C (faster) implementation if possible
|
||||||
try:
|
try:
|
||||||
@ -44,17 +44,17 @@ class Serializer(PythonSerializer):
|
|||||||
|
|
||||||
internal_use_only = False
|
internal_use_only = False
|
||||||
|
|
||||||
def handle_field(self, obj, field):
|
def _value_from_field(self, obj, field):
|
||||||
# A nasty special case: base YAML doesn't support serialization of time
|
# A nasty special case: base YAML doesn't support serialization of time
|
||||||
# types (as opposed to dates or datetimes, which it does support). Since
|
# types (as opposed to dates or datetimes, which it does support). Since
|
||||||
# we want to use the "safe" serializer for better interoperability, we
|
# we want to use the "safe" serializer for better interoperability, we
|
||||||
# need to do something with those pesky times. Converting 'em to strings
|
# need to do something with those pesky times. Converting 'em to strings
|
||||||
# isn't perfect, but it's better than a "!!python/time" type which would
|
# isn't perfect, but it's better than a "!!python/time" type which would
|
||||||
# halt deserialization under any other language.
|
# halt deserialization under any other language.
|
||||||
if isinstance(field, models.TimeField) and getattr(obj, field.name) is not None:
|
value = super()._value_from_field(obj, field)
|
||||||
self._current[field.name] = str(getattr(obj, field.name))
|
if isinstance(value, datetime.time):
|
||||||
else:
|
value = str(value)
|
||||||
super().handle_field(obj, field)
|
return value
|
||||||
|
|
||||||
def end_serialization(self):
|
def end_serialization(self):
|
||||||
self.options.setdefault("allow_unicode", True)
|
self.options.setdefault("allow_unicode", True)
|
||||||
|
@ -101,13 +101,16 @@ class DomainNameValidator(RegexValidator):
|
|||||||
|
|
||||||
if self.accept_idna:
|
if self.accept_idna:
|
||||||
self.regex = _lazy_re_compile(
|
self.regex = _lazy_re_compile(
|
||||||
self.hostname_re + self.domain_re + self.tld_re, re.IGNORECASE
|
r"^" + self.hostname_re + self.domain_re + self.tld_re + r"$",
|
||||||
|
re.IGNORECASE,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.regex = _lazy_re_compile(
|
self.regex = _lazy_re_compile(
|
||||||
self.ascii_only_hostname_re
|
r"^"
|
||||||
|
+ self.ascii_only_hostname_re
|
||||||
+ self.ascii_only_domain_re
|
+ self.ascii_only_domain_re
|
||||||
+ self.ascii_only_tld_re,
|
+ self.ascii_only_tld_re
|
||||||
|
+ r"$",
|
||||||
re.IGNORECASE,
|
re.IGNORECASE,
|
||||||
)
|
)
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
@ -215,7 +215,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
|||||||
def get_connection_params(self):
|
def get_connection_params(self):
|
||||||
kwargs = {
|
kwargs = {
|
||||||
"conv": django_conversions,
|
"conv": django_conversions,
|
||||||
"charset": "utf8",
|
"charset": "utf8mb4",
|
||||||
}
|
}
|
||||||
settings_dict = self.settings_dict
|
settings_dict = self.settings_dict
|
||||||
if settings_dict["USER"]:
|
if settings_dict["USER"]:
|
||||||
|
@ -71,21 +71,11 @@ class DatabaseFeatures(BaseDatabaseFeatures):
|
|||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def test_collations(self):
|
def test_collations(self):
|
||||||
charset = "utf8"
|
|
||||||
if (
|
|
||||||
self.connection.mysql_is_mariadb
|
|
||||||
and self.connection.mysql_version >= (10, 6)
|
|
||||||
) or (
|
|
||||||
not self.connection.mysql_is_mariadb
|
|
||||||
and self.connection.mysql_version >= (8, 0, 30)
|
|
||||||
):
|
|
||||||
# utf8 is an alias for utf8mb3 in MariaDB 10.6+ and MySQL 8.0.30+.
|
|
||||||
charset = "utf8mb3"
|
|
||||||
return {
|
return {
|
||||||
"ci": f"{charset}_general_ci",
|
"ci": "utf8mb4_general_ci",
|
||||||
"non_default": f"{charset}_esperanto_ci",
|
"non_default": "utf8mb4_esperanto_ci",
|
||||||
"swedish_ci": f"{charset}_swedish_ci",
|
"swedish_ci": "utf8mb4_swedish_ci",
|
||||||
"virtual": f"{charset}_esperanto_ci",
|
"virtual": "utf8mb4_esperanto_ci",
|
||||||
}
|
}
|
||||||
|
|
||||||
test_now_utc_template = "UTC_TIMESTAMP(6)"
|
test_now_utc_template = "UTC_TIMESTAMP(6)"
|
||||||
@ -99,10 +89,6 @@ class DatabaseFeatures(BaseDatabaseFeatures):
|
|||||||
"db_functions.comparison.test_least.LeastTests."
|
"db_functions.comparison.test_least.LeastTests."
|
||||||
"test_coalesce_workaround",
|
"test_coalesce_workaround",
|
||||||
},
|
},
|
||||||
"Running on MySQL requires utf8mb4 encoding (#18392).": {
|
|
||||||
"model_fields.test_textfield.TextFieldTests.test_emoji",
|
|
||||||
"model_fields.test_charfield.TestCharField.test_emoji",
|
|
||||||
},
|
|
||||||
"MySQL doesn't support functional indexes on a function that "
|
"MySQL doesn't support functional indexes on a function that "
|
||||||
"returns JSON": {
|
"returns JSON": {
|
||||||
"schema.tests.SchemaTests.test_func_index_json_key_transform",
|
"schema.tests.SchemaTests.test_func_index_json_key_transform",
|
||||||
|
@ -160,6 +160,10 @@ class DatabaseFeatures(BaseDatabaseFeatures):
|
|||||||
def is_postgresql_16(self):
|
def is_postgresql_16(self):
|
||||||
return self.connection.pg_version >= 160000
|
return self.connection.pg_version >= 160000
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def is_postgresql_17(self):
|
||||||
|
return self.connection.pg_version >= 170000
|
||||||
|
|
||||||
supports_unlimited_charfield = True
|
supports_unlimited_charfield = True
|
||||||
supports_nulls_distinct_unique_constraints = property(
|
supports_nulls_distinct_unique_constraints = property(
|
||||||
operator.attrgetter("is_postgresql_15")
|
operator.attrgetter("is_postgresql_15")
|
||||||
|
@ -32,7 +32,9 @@ class DatabaseOperations(BaseDatabaseOperations):
|
|||||||
"BUFFERS",
|
"BUFFERS",
|
||||||
"COSTS",
|
"COSTS",
|
||||||
"GENERIC_PLAN",
|
"GENERIC_PLAN",
|
||||||
|
"MEMORY",
|
||||||
"SETTINGS",
|
"SETTINGS",
|
||||||
|
"SERIALIZE",
|
||||||
"SUMMARY",
|
"SUMMARY",
|
||||||
"TIMING",
|
"TIMING",
|
||||||
"VERBOSE",
|
"VERBOSE",
|
||||||
@ -365,6 +367,9 @@ class DatabaseOperations(BaseDatabaseOperations):
|
|||||||
|
|
||||||
def explain_query_prefix(self, format=None, **options):
|
def explain_query_prefix(self, format=None, **options):
|
||||||
extra = {}
|
extra = {}
|
||||||
|
if serialize := options.pop("serialize", None):
|
||||||
|
if serialize.upper() in {"TEXT", "BINARY"}:
|
||||||
|
extra["SERIALIZE"] = serialize.upper()
|
||||||
# Normalize options.
|
# Normalize options.
|
||||||
if options:
|
if options:
|
||||||
options = {
|
options = {
|
||||||
|
@ -140,6 +140,13 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
|
|||||||
return sequence["name"]
|
return sequence["name"]
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def _is_changing_type_of_indexed_text_column(self, old_field, old_type, new_type):
|
||||||
|
return (old_field.db_index or old_field.unique) and (
|
||||||
|
(old_type.startswith("varchar") and not new_type.startswith("varchar"))
|
||||||
|
or (old_type.startswith("text") and not new_type.startswith("text"))
|
||||||
|
or (old_type.startswith("citext") and not new_type.startswith("citext"))
|
||||||
|
)
|
||||||
|
|
||||||
def _alter_column_type_sql(
|
def _alter_column_type_sql(
|
||||||
self, model, old_field, new_field, new_type, old_collation, new_collation
|
self, model, old_field, new_field, new_type, old_collation, new_collation
|
||||||
):
|
):
|
||||||
@ -147,11 +154,7 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
|
|||||||
# different type.
|
# different type.
|
||||||
old_db_params = old_field.db_parameters(connection=self.connection)
|
old_db_params = old_field.db_parameters(connection=self.connection)
|
||||||
old_type = old_db_params["type"]
|
old_type = old_db_params["type"]
|
||||||
if (old_field.db_index or old_field.unique) and (
|
if self._is_changing_type_of_indexed_text_column(old_field, old_type, new_type):
|
||||||
(old_type.startswith("varchar") and not new_type.startswith("varchar"))
|
|
||||||
or (old_type.startswith("text") and not new_type.startswith("text"))
|
|
||||||
or (old_type.startswith("citext") and not new_type.startswith("citext"))
|
|
||||||
):
|
|
||||||
index_name = self._create_index_name(
|
index_name = self._create_index_name(
|
||||||
model._meta.db_table, [old_field.column], suffix="_like"
|
model._meta.db_table, [old_field.column], suffix="_like"
|
||||||
)
|
)
|
||||||
@ -277,8 +280,14 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
|
|||||||
strict,
|
strict,
|
||||||
)
|
)
|
||||||
# Added an index? Create any PostgreSQL-specific indexes.
|
# Added an index? Create any PostgreSQL-specific indexes.
|
||||||
if (not (old_field.db_index or old_field.unique) and new_field.db_index) or (
|
if (
|
||||||
not old_field.unique and new_field.unique
|
(not (old_field.db_index or old_field.unique) and new_field.db_index)
|
||||||
|
or (not old_field.unique and new_field.unique)
|
||||||
|
or (
|
||||||
|
self._is_changing_type_of_indexed_text_column(
|
||||||
|
old_field, old_type, new_type
|
||||||
|
)
|
||||||
|
)
|
||||||
):
|
):
|
||||||
like_index_statement = self._create_like_index_sql(model, new_field)
|
like_index_statement = self._create_like_index_sql(model, new_field)
|
||||||
if like_index_statement is not None:
|
if like_index_statement is not None:
|
||||||
|
@ -101,7 +101,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
|
|||||||
"servers.tests.LiveServerTestCloseConnectionTest."
|
"servers.tests.LiveServerTestCloseConnectionTest."
|
||||||
"test_closes_connections",
|
"test_closes_connections",
|
||||||
},
|
},
|
||||||
"For SQLite in-memory tests, closing the connection destroys"
|
"For SQLite in-memory tests, closing the connection destroys "
|
||||||
"the database.": {
|
"the database.": {
|
||||||
"test_utils.tests.AssertNumQueriesUponConnectionTests."
|
"test_utils.tests.AssertNumQueriesUponConnectionTests."
|
||||||
"test_ignores_connection_configuration_queries",
|
"test_ignores_connection_configuration_queries",
|
||||||
|
@ -219,6 +219,7 @@ class MigrationAutodetector:
|
|||||||
self.generate_altered_unique_together()
|
self.generate_altered_unique_together()
|
||||||
self.generate_added_indexes()
|
self.generate_added_indexes()
|
||||||
self.generate_added_constraints()
|
self.generate_added_constraints()
|
||||||
|
self.generate_altered_constraints()
|
||||||
self.generate_altered_db_table()
|
self.generate_altered_db_table()
|
||||||
|
|
||||||
self._sort_migrations()
|
self._sort_migrations()
|
||||||
@ -1450,6 +1451,19 @@ class MigrationAutodetector:
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _constraint_should_be_dropped_and_recreated(
|
||||||
|
self, old_constraint, new_constraint
|
||||||
|
):
|
||||||
|
old_path, old_args, old_kwargs = old_constraint.deconstruct()
|
||||||
|
new_path, new_args, new_kwargs = new_constraint.deconstruct()
|
||||||
|
|
||||||
|
for attr in old_constraint.non_db_attrs:
|
||||||
|
old_kwargs.pop(attr, None)
|
||||||
|
for attr in new_constraint.non_db_attrs:
|
||||||
|
new_kwargs.pop(attr, None)
|
||||||
|
|
||||||
|
return (old_path, old_args, old_kwargs) != (new_path, new_args, new_kwargs)
|
||||||
|
|
||||||
def create_altered_constraints(self):
|
def create_altered_constraints(self):
|
||||||
option_name = operations.AddConstraint.option_name
|
option_name = operations.AddConstraint.option_name
|
||||||
for app_label, model_name in sorted(self.kept_model_keys):
|
for app_label, model_name in sorted(self.kept_model_keys):
|
||||||
@ -1461,14 +1475,41 @@ class MigrationAutodetector:
|
|||||||
|
|
||||||
old_constraints = old_model_state.options[option_name]
|
old_constraints = old_model_state.options[option_name]
|
||||||
new_constraints = new_model_state.options[option_name]
|
new_constraints = new_model_state.options[option_name]
|
||||||
add_constraints = [c for c in new_constraints if c not in old_constraints]
|
|
||||||
rem_constraints = [c for c in old_constraints if c not in new_constraints]
|
alt_constraints = []
|
||||||
|
alt_constraints_name = []
|
||||||
|
|
||||||
|
for old_c in old_constraints:
|
||||||
|
for new_c in new_constraints:
|
||||||
|
old_c_dec = old_c.deconstruct()
|
||||||
|
new_c_dec = new_c.deconstruct()
|
||||||
|
if (
|
||||||
|
old_c_dec != new_c_dec
|
||||||
|
and old_c.name == new_c.name
|
||||||
|
and not self._constraint_should_be_dropped_and_recreated(
|
||||||
|
old_c, new_c
|
||||||
|
)
|
||||||
|
):
|
||||||
|
alt_constraints.append(new_c)
|
||||||
|
alt_constraints_name.append(new_c.name)
|
||||||
|
|
||||||
|
add_constraints = [
|
||||||
|
c
|
||||||
|
for c in new_constraints
|
||||||
|
if c not in old_constraints and c.name not in alt_constraints_name
|
||||||
|
]
|
||||||
|
rem_constraints = [
|
||||||
|
c
|
||||||
|
for c in old_constraints
|
||||||
|
if c not in new_constraints and c.name not in alt_constraints_name
|
||||||
|
]
|
||||||
|
|
||||||
self.altered_constraints.update(
|
self.altered_constraints.update(
|
||||||
{
|
{
|
||||||
(app_label, model_name): {
|
(app_label, model_name): {
|
||||||
"added_constraints": add_constraints,
|
"added_constraints": add_constraints,
|
||||||
"removed_constraints": rem_constraints,
|
"removed_constraints": rem_constraints,
|
||||||
|
"altered_constraints": alt_constraints,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -1503,6 +1544,23 @@ class MigrationAutodetector:
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def generate_altered_constraints(self):
|
||||||
|
for (
|
||||||
|
app_label,
|
||||||
|
model_name,
|
||||||
|
), alt_constraints in self.altered_constraints.items():
|
||||||
|
dependencies = self._get_dependencies_for_model(app_label, model_name)
|
||||||
|
for constraint in alt_constraints["altered_constraints"]:
|
||||||
|
self.add_operation(
|
||||||
|
app_label,
|
||||||
|
operations.AlterConstraint(
|
||||||
|
model_name=model_name,
|
||||||
|
name=constraint.name,
|
||||||
|
constraint=constraint,
|
||||||
|
),
|
||||||
|
dependencies=dependencies,
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_dependencies_for_foreign_key(app_label, model_name, field, project_state):
|
def _get_dependencies_for_foreign_key(app_label, model_name, field, project_state):
|
||||||
remote_field_model = None
|
remote_field_model = None
|
||||||
|
@ -2,6 +2,7 @@ from .fields import AddField, AlterField, RemoveField, RenameField
|
|||||||
from .models import (
|
from .models import (
|
||||||
AddConstraint,
|
AddConstraint,
|
||||||
AddIndex,
|
AddIndex,
|
||||||
|
AlterConstraint,
|
||||||
AlterIndexTogether,
|
AlterIndexTogether,
|
||||||
AlterModelManagers,
|
AlterModelManagers,
|
||||||
AlterModelOptions,
|
AlterModelOptions,
|
||||||
@ -36,6 +37,7 @@ __all__ = [
|
|||||||
"RenameField",
|
"RenameField",
|
||||||
"AddConstraint",
|
"AddConstraint",
|
||||||
"RemoveConstraint",
|
"RemoveConstraint",
|
||||||
|
"AlterConstraint",
|
||||||
"SeparateDatabaseAndState",
|
"SeparateDatabaseAndState",
|
||||||
"RunSQL",
|
"RunSQL",
|
||||||
"RunPython",
|
"RunPython",
|
||||||
|
@ -1230,6 +1230,12 @@ class AddConstraint(IndexOperation):
|
|||||||
and self.constraint.name == operation.name
|
and self.constraint.name == operation.name
|
||||||
):
|
):
|
||||||
return []
|
return []
|
||||||
|
if (
|
||||||
|
isinstance(operation, AlterConstraint)
|
||||||
|
and self.model_name_lower == operation.model_name_lower
|
||||||
|
and self.constraint.name == operation.name
|
||||||
|
):
|
||||||
|
return [AddConstraint(self.model_name, operation.constraint)]
|
||||||
return super().reduce(operation, app_label)
|
return super().reduce(operation, app_label)
|
||||||
|
|
||||||
|
|
||||||
@ -1274,3 +1280,51 @@ class RemoveConstraint(IndexOperation):
|
|||||||
@property
|
@property
|
||||||
def migration_name_fragment(self):
|
def migration_name_fragment(self):
|
||||||
return "remove_%s_%s" % (self.model_name_lower, self.name.lower())
|
return "remove_%s_%s" % (self.model_name_lower, self.name.lower())
|
||||||
|
|
||||||
|
|
||||||
|
class AlterConstraint(IndexOperation):
|
||||||
|
category = OperationCategory.ALTERATION
|
||||||
|
option_name = "constraints"
|
||||||
|
|
||||||
|
def __init__(self, model_name, name, constraint):
|
||||||
|
self.model_name = model_name
|
||||||
|
self.name = name
|
||||||
|
self.constraint = constraint
|
||||||
|
|
||||||
|
def state_forwards(self, app_label, state):
|
||||||
|
state.alter_constraint(
|
||||||
|
app_label, self.model_name_lower, self.name, self.constraint
|
||||||
|
)
|
||||||
|
|
||||||
|
def database_forwards(self, app_label, schema_editor, from_state, to_state):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def database_backwards(self, app_label, schema_editor, from_state, to_state):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def deconstruct(self):
|
||||||
|
return (
|
||||||
|
self.__class__.__name__,
|
||||||
|
[],
|
||||||
|
{
|
||||||
|
"model_name": self.model_name,
|
||||||
|
"name": self.name,
|
||||||
|
"constraint": self.constraint,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def describe(self):
|
||||||
|
return f"Alter constraint {self.name} on {self.model_name}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def migration_name_fragment(self):
|
||||||
|
return "alter_%s_%s" % (self.model_name_lower, self.constraint.name.lower())
|
||||||
|
|
||||||
|
def reduce(self, operation, app_label):
|
||||||
|
if (
|
||||||
|
isinstance(operation, (AlterConstraint, RemoveConstraint))
|
||||||
|
and self.model_name_lower == operation.model_name_lower
|
||||||
|
and self.name == operation.name
|
||||||
|
):
|
||||||
|
return [operation]
|
||||||
|
return super().reduce(operation, app_label)
|
||||||
|
@ -160,8 +160,8 @@ class InteractiveMigrationQuestioner(MigrationQuestioner):
|
|||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
return eval(code, {}, {"datetime": datetime, "timezone": timezone})
|
return eval(code, {}, {"datetime": datetime, "timezone": timezone})
|
||||||
except (SyntaxError, NameError) as e:
|
except Exception as e:
|
||||||
self.prompt_output.write("Invalid input: %s" % e)
|
self.prompt_output.write(f"{e.__class__.__name__}: {e}")
|
||||||
|
|
||||||
def ask_not_null_addition(self, field_name, model_name):
|
def ask_not_null_addition(self, field_name, model_name):
|
||||||
"""Adding a NOT NULL field to a model."""
|
"""Adding a NOT NULL field to a model."""
|
||||||
|
@ -211,6 +211,14 @@ class ProjectState:
|
|||||||
model_state.options[option_name] = [obj for obj in objs if obj.name != obj_name]
|
model_state.options[option_name] = [obj for obj in objs if obj.name != obj_name]
|
||||||
self.reload_model(app_label, model_name, delay=True)
|
self.reload_model(app_label, model_name, delay=True)
|
||||||
|
|
||||||
|
def _alter_option(self, app_label, model_name, option_name, obj_name, alt_obj):
|
||||||
|
model_state = self.models[app_label, model_name]
|
||||||
|
objs = model_state.options[option_name]
|
||||||
|
model_state.options[option_name] = [
|
||||||
|
obj if obj.name != obj_name else alt_obj for obj in objs
|
||||||
|
]
|
||||||
|
self.reload_model(app_label, model_name, delay=True)
|
||||||
|
|
||||||
def add_index(self, app_label, model_name, index):
|
def add_index(self, app_label, model_name, index):
|
||||||
self._append_option(app_label, model_name, "indexes", index)
|
self._append_option(app_label, model_name, "indexes", index)
|
||||||
|
|
||||||
@ -237,6 +245,11 @@ class ProjectState:
|
|||||||
def remove_constraint(self, app_label, model_name, constraint_name):
|
def remove_constraint(self, app_label, model_name, constraint_name):
|
||||||
self._remove_option(app_label, model_name, "constraints", constraint_name)
|
self._remove_option(app_label, model_name, "constraints", constraint_name)
|
||||||
|
|
||||||
|
def alter_constraint(self, app_label, model_name, constraint_name, constraint):
|
||||||
|
self._alter_option(
|
||||||
|
app_label, model_name, "constraints", constraint_name, constraint
|
||||||
|
)
|
||||||
|
|
||||||
def add_field(self, app_label, model_name, name, field, preserve_default):
|
def add_field(self, app_label, model_name, name, field, preserve_default):
|
||||||
# If preserve default is off, don't use the default for future state.
|
# If preserve default is off, don't use the default for future state.
|
||||||
if not preserve_default:
|
if not preserve_default:
|
||||||
|
@ -23,6 +23,8 @@ class BaseConstraint:
|
|||||||
violation_error_code = None
|
violation_error_code = None
|
||||||
violation_error_message = None
|
violation_error_message = None
|
||||||
|
|
||||||
|
non_db_attrs = ("violation_error_code", "violation_error_message")
|
||||||
|
|
||||||
# RemovedInDjango60Warning: When the deprecation ends, replace with:
|
# RemovedInDjango60Warning: When the deprecation ends, replace with:
|
||||||
# def __init__(
|
# def __init__(
|
||||||
# self, *, name, violation_error_code=None, violation_error_message=None
|
# self, *, name, violation_error_code=None, violation_error_message=None
|
||||||
|
@ -392,7 +392,10 @@ class Field(RegisterLookupMixin):
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
self.db_default is NOT_PROVIDED
|
self.db_default is NOT_PROVIDED
|
||||||
or isinstance(self.db_default, Value)
|
or (
|
||||||
|
isinstance(self.db_default, Value)
|
||||||
|
or not hasattr(self.db_default, "resolve_expression")
|
||||||
|
)
|
||||||
or databases is None
|
or databases is None
|
||||||
):
|
):
|
||||||
return []
|
return []
|
||||||
|
@ -2,7 +2,7 @@ import itertools
|
|||||||
|
|
||||||
from django.core.exceptions import EmptyResultSet
|
from django.core.exceptions import EmptyResultSet
|
||||||
from django.db.models import Field
|
from django.db.models import Field
|
||||||
from django.db.models.expressions import Func, Value
|
from django.db.models.expressions import ColPairs, Func, Value
|
||||||
from django.db.models.lookups import (
|
from django.db.models.lookups import (
|
||||||
Exact,
|
Exact,
|
||||||
GreaterThan,
|
GreaterThan,
|
||||||
@ -12,6 +12,7 @@ from django.db.models.lookups import (
|
|||||||
LessThan,
|
LessThan,
|
||||||
LessThanOrEqual,
|
LessThanOrEqual,
|
||||||
)
|
)
|
||||||
|
from django.db.models.sql import Query
|
||||||
from django.db.models.sql.where import AND, OR, WhereNode
|
from django.db.models.sql.where import AND, OR, WhereNode
|
||||||
|
|
||||||
|
|
||||||
@ -28,17 +29,32 @@ class Tuple(Func):
|
|||||||
|
|
||||||
class TupleLookupMixin:
|
class TupleLookupMixin:
|
||||||
def get_prep_lookup(self):
|
def get_prep_lookup(self):
|
||||||
|
self.check_rhs_is_tuple_or_list()
|
||||||
self.check_rhs_length_equals_lhs_length()
|
self.check_rhs_length_equals_lhs_length()
|
||||||
return self.rhs
|
return self.rhs
|
||||||
|
|
||||||
|
def check_rhs_is_tuple_or_list(self):
|
||||||
|
if not isinstance(self.rhs, (tuple, list)):
|
||||||
|
lhs_str = self.get_lhs_str()
|
||||||
|
raise ValueError(
|
||||||
|
f"{self.lookup_name!r} lookup of {lhs_str} must be a tuple or a list"
|
||||||
|
)
|
||||||
|
|
||||||
def check_rhs_length_equals_lhs_length(self):
|
def check_rhs_length_equals_lhs_length(self):
|
||||||
len_lhs = len(self.lhs)
|
len_lhs = len(self.lhs)
|
||||||
if len_lhs != len(self.rhs):
|
if len_lhs != len(self.rhs):
|
||||||
|
lhs_str = self.get_lhs_str()
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"'{self.lookup_name}' lookup of '{self.lhs.field.name}' field "
|
f"{self.lookup_name!r} lookup of {lhs_str} must have {len_lhs} elements"
|
||||||
f"must have {len_lhs} elements"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_lhs_str(self):
|
||||||
|
if isinstance(self.lhs, ColPairs):
|
||||||
|
return repr(self.lhs.field.name)
|
||||||
|
else:
|
||||||
|
names = ", ".join(repr(f.name) for f in self.lhs)
|
||||||
|
return f"({names})"
|
||||||
|
|
||||||
def get_prep_lhs(self):
|
def get_prep_lhs(self):
|
||||||
if isinstance(self.lhs, (tuple, list)):
|
if isinstance(self.lhs, (tuple, list)):
|
||||||
return Tuple(*self.lhs)
|
return Tuple(*self.lhs)
|
||||||
@ -196,17 +212,52 @@ class TupleLessThanOrEqual(TupleLookupMixin, LessThanOrEqual):
|
|||||||
|
|
||||||
class TupleIn(TupleLookupMixin, In):
|
class TupleIn(TupleLookupMixin, In):
|
||||||
def get_prep_lookup(self):
|
def get_prep_lookup(self):
|
||||||
self.check_rhs_elements_length_equals_lhs_length()
|
if self.rhs_is_direct_value():
|
||||||
return super(TupleLookupMixin, self).get_prep_lookup()
|
self.check_rhs_is_tuple_or_list()
|
||||||
|
self.check_rhs_is_collection_of_tuples_or_lists()
|
||||||
|
self.check_rhs_elements_length_equals_lhs_length()
|
||||||
|
else:
|
||||||
|
self.check_rhs_is_query()
|
||||||
|
self.check_rhs_select_length_equals_lhs_length()
|
||||||
|
|
||||||
|
return self.rhs # skip checks from mixin
|
||||||
|
|
||||||
|
def check_rhs_is_collection_of_tuples_or_lists(self):
|
||||||
|
if not all(isinstance(vals, (tuple, list)) for vals in self.rhs):
|
||||||
|
lhs_str = self.get_lhs_str()
|
||||||
|
raise ValueError(
|
||||||
|
f"{self.lookup_name!r} lookup of {lhs_str} "
|
||||||
|
"must be a collection of tuples or lists"
|
||||||
|
)
|
||||||
|
|
||||||
def check_rhs_elements_length_equals_lhs_length(self):
|
def check_rhs_elements_length_equals_lhs_length(self):
|
||||||
len_lhs = len(self.lhs)
|
len_lhs = len(self.lhs)
|
||||||
if not all(len_lhs == len(vals) for vals in self.rhs):
|
if not all(len_lhs == len(vals) for vals in self.rhs):
|
||||||
|
lhs_str = self.get_lhs_str()
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"'{self.lookup_name}' lookup of '{self.lhs.field.name}' field "
|
f"{self.lookup_name!r} lookup of {lhs_str} "
|
||||||
f"must have {len_lhs} elements each"
|
f"must have {len_lhs} elements each"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def check_rhs_is_query(self):
|
||||||
|
if not isinstance(self.rhs, Query):
|
||||||
|
lhs_str = self.get_lhs_str()
|
||||||
|
rhs_cls = self.rhs.__class__.__name__
|
||||||
|
raise ValueError(
|
||||||
|
f"{self.lookup_name!r} subquery lookup of {lhs_str} "
|
||||||
|
f"must be a Query object (received {rhs_cls!r})"
|
||||||
|
)
|
||||||
|
|
||||||
|
def check_rhs_select_length_equals_lhs_length(self):
|
||||||
|
len_rhs = len(self.rhs.select)
|
||||||
|
len_lhs = len(self.lhs)
|
||||||
|
if len_rhs != len_lhs:
|
||||||
|
lhs_str = self.get_lhs_str()
|
||||||
|
raise ValueError(
|
||||||
|
f"{self.lookup_name!r} subquery lookup of {lhs_str} "
|
||||||
|
f"must have {len_lhs} fields (received {len_rhs})"
|
||||||
|
)
|
||||||
|
|
||||||
def process_rhs(self, compiler, connection):
|
def process_rhs(self, compiler, connection):
|
||||||
rhs = self.rhs
|
rhs = self.rhs
|
||||||
if not rhs:
|
if not rhs:
|
||||||
@ -229,10 +280,17 @@ class TupleIn(TupleLookupMixin, In):
|
|||||||
|
|
||||||
return Tuple(*result).as_sql(compiler, connection)
|
return Tuple(*result).as_sql(compiler, connection)
|
||||||
|
|
||||||
|
def as_sql(self, compiler, connection):
|
||||||
|
if not self.rhs_is_direct_value():
|
||||||
|
return self.as_subquery(compiler, connection)
|
||||||
|
return super().as_sql(compiler, connection)
|
||||||
|
|
||||||
def as_sqlite(self, compiler, connection):
|
def as_sqlite(self, compiler, connection):
|
||||||
rhs = self.rhs
|
rhs = self.rhs
|
||||||
if not rhs:
|
if not rhs:
|
||||||
raise EmptyResultSet
|
raise EmptyResultSet
|
||||||
|
if not self.rhs_is_direct_value():
|
||||||
|
return self.as_subquery(compiler, connection)
|
||||||
|
|
||||||
# e.g.: (a, b, c) in [(x1, y1, z1), (x2, y2, z2)] as SQL:
|
# e.g.: (a, b, c) in [(x1, y1, z1), (x2, y2, z2)] as SQL:
|
||||||
# WHERE (a = x1 AND b = y1 AND c = z1) OR (a = x2 AND b = y2 AND c = z2)
|
# WHERE (a = x1 AND b = y1 AND c = z1) OR (a = x2 AND b = y2 AND c = z2)
|
||||||
@ -245,6 +303,9 @@ class TupleIn(TupleLookupMixin, In):
|
|||||||
|
|
||||||
return root.as_sql(compiler, connection)
|
return root.as_sql(compiler, connection)
|
||||||
|
|
||||||
|
def as_subquery(self, compiler, connection):
|
||||||
|
return compiler.compile(In(self.lhs, self.rhs))
|
||||||
|
|
||||||
|
|
||||||
tuple_lookups = {
|
tuple_lookups = {
|
||||||
"exact": TupleExact,
|
"exact": TupleExact,
|
||||||
|
@ -160,39 +160,43 @@ class JSONObject(Func):
|
|||||||
)
|
)
|
||||||
return super().as_sql(compiler, connection, **extra_context)
|
return super().as_sql(compiler, connection, **extra_context)
|
||||||
|
|
||||||
def as_native(self, compiler, connection, *, returning, **extra_context):
|
def join(self, args):
|
||||||
class ArgJoiner:
|
pairs = zip(args[::2], args[1::2], strict=True)
|
||||||
def join(self, args):
|
# Wrap 'key' in parentheses in case of postgres cast :: syntax.
|
||||||
pairs = zip(args[::2], args[1::2], strict=True)
|
return ", ".join([f"({key}) VALUE {value}" for key, value in pairs])
|
||||||
return ", ".join([" VALUE ".join(pair) for pair in pairs])
|
|
||||||
|
|
||||||
|
def as_native(self, compiler, connection, *, returning, **extra_context):
|
||||||
return self.as_sql(
|
return self.as_sql(
|
||||||
compiler,
|
compiler,
|
||||||
connection,
|
connection,
|
||||||
arg_joiner=ArgJoiner(),
|
arg_joiner=self,
|
||||||
template=f"%(function)s(%(expressions)s RETURNING {returning})",
|
template=f"%(function)s(%(expressions)s RETURNING {returning})",
|
||||||
**extra_context,
|
**extra_context,
|
||||||
)
|
)
|
||||||
|
|
||||||
def as_postgresql(self, compiler, connection, **extra_context):
|
def as_postgresql(self, compiler, connection, **extra_context):
|
||||||
if (
|
# Casting keys to text is only required when using JSONB_BUILD_OBJECT
|
||||||
not connection.features.is_postgresql_16
|
# or when using JSON_OBJECT on PostgreSQL 16+ with server-side bindings.
|
||||||
or connection.features.uses_server_side_binding
|
# This is done in all cases for consistency.
|
||||||
):
|
copy = self.copy()
|
||||||
copy = self.copy()
|
copy.set_source_expressions(
|
||||||
copy.set_source_expressions(
|
[
|
||||||
[
|
Cast(expression, TextField()) if index % 2 == 0 else expression
|
||||||
Cast(expression, TextField()) if index % 2 == 0 else expression
|
for index, expression in enumerate(copy.get_source_expressions())
|
||||||
for index, expression in enumerate(copy.get_source_expressions())
|
]
|
||||||
]
|
)
|
||||||
|
|
||||||
|
if connection.features.is_postgresql_16:
|
||||||
|
return copy.as_native(
|
||||||
|
compiler, connection, returning="JSONB", **extra_context
|
||||||
)
|
)
|
||||||
return super(JSONObject, copy).as_sql(
|
|
||||||
compiler,
|
return super(JSONObject, copy).as_sql(
|
||||||
connection,
|
compiler,
|
||||||
function="JSONB_BUILD_OBJECT",
|
connection,
|
||||||
**extra_context,
|
function="JSONB_BUILD_OBJECT",
|
||||||
)
|
**extra_context,
|
||||||
return self.as_native(compiler, connection, returning="JSONB", **extra_context)
|
)
|
||||||
|
|
||||||
def as_oracle(self, compiler, connection, **extra_context):
|
def as_oracle(self, compiler, connection, **extra_context):
|
||||||
return self.as_native(compiler, connection, returning="CLOB", **extra_context)
|
return self.as_native(compiler, connection, returning="CLOB", **extra_context)
|
||||||
|
@ -660,9 +660,13 @@ class QuerySet(AltersData):
|
|||||||
obj.save(force_insert=True, using=self.db)
|
obj.save(force_insert=True, using=self.db)
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
create.alters_data = True
|
||||||
|
|
||||||
async def acreate(self, **kwargs):
|
async def acreate(self, **kwargs):
|
||||||
return await sync_to_async(self.create)(**kwargs)
|
return await sync_to_async(self.create)(**kwargs)
|
||||||
|
|
||||||
|
acreate.alters_data = True
|
||||||
|
|
||||||
def _prepare_for_bulk_create(self, objs):
|
def _prepare_for_bulk_create(self, objs):
|
||||||
from django.db.models.expressions import DatabaseDefault
|
from django.db.models.expressions import DatabaseDefault
|
||||||
|
|
||||||
@ -835,6 +839,8 @@ class QuerySet(AltersData):
|
|||||||
|
|
||||||
return objs
|
return objs
|
||||||
|
|
||||||
|
bulk_create.alters_data = True
|
||||||
|
|
||||||
async def abulk_create(
|
async def abulk_create(
|
||||||
self,
|
self,
|
||||||
objs,
|
objs,
|
||||||
@ -853,6 +859,8 @@ class QuerySet(AltersData):
|
|||||||
unique_fields=unique_fields,
|
unique_fields=unique_fields,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
abulk_create.alters_data = True
|
||||||
|
|
||||||
def bulk_update(self, objs, fields, batch_size=None):
|
def bulk_update(self, objs, fields, batch_size=None):
|
||||||
"""
|
"""
|
||||||
Update the given fields in each of the given objects in the database.
|
Update the given fields in each of the given objects in the database.
|
||||||
@ -941,12 +949,16 @@ class QuerySet(AltersData):
|
|||||||
pass
|
pass
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
get_or_create.alters_data = True
|
||||||
|
|
||||||
async def aget_or_create(self, defaults=None, **kwargs):
|
async def aget_or_create(self, defaults=None, **kwargs):
|
||||||
return await sync_to_async(self.get_or_create)(
|
return await sync_to_async(self.get_or_create)(
|
||||||
defaults=defaults,
|
defaults=defaults,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
aget_or_create.alters_data = True
|
||||||
|
|
||||||
def update_or_create(self, defaults=None, create_defaults=None, **kwargs):
|
def update_or_create(self, defaults=None, create_defaults=None, **kwargs):
|
||||||
"""
|
"""
|
||||||
Look up an object with the given kwargs, updating one with defaults
|
Look up an object with the given kwargs, updating one with defaults
|
||||||
@ -992,6 +1004,8 @@ class QuerySet(AltersData):
|
|||||||
obj.save(using=self.db)
|
obj.save(using=self.db)
|
||||||
return obj, False
|
return obj, False
|
||||||
|
|
||||||
|
update_or_create.alters_data = True
|
||||||
|
|
||||||
async def aupdate_or_create(self, defaults=None, create_defaults=None, **kwargs):
|
async def aupdate_or_create(self, defaults=None, create_defaults=None, **kwargs):
|
||||||
return await sync_to_async(self.update_or_create)(
|
return await sync_to_async(self.update_or_create)(
|
||||||
defaults=defaults,
|
defaults=defaults,
|
||||||
@ -999,6 +1013,8 @@ class QuerySet(AltersData):
|
|||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
aupdate_or_create.alters_data = True
|
||||||
|
|
||||||
def _extract_model_params(self, defaults, **kwargs):
|
def _extract_model_params(self, defaults, **kwargs):
|
||||||
"""
|
"""
|
||||||
Prepare `params` for creating a model instance based on the given
|
Prepare `params` for creating a model instance based on the given
|
||||||
|
@ -1021,11 +1021,21 @@ class Query(BaseExpression):
|
|||||||
if alias == old_alias:
|
if alias == old_alias:
|
||||||
table_aliases[pos] = new_alias
|
table_aliases[pos] = new_alias
|
||||||
break
|
break
|
||||||
|
|
||||||
|
# 3. Rename the direct external aliases and the ones of combined
|
||||||
|
# queries (union, intersection, difference).
|
||||||
self.external_aliases = {
|
self.external_aliases = {
|
||||||
# Table is aliased or it's being changed and thus is aliased.
|
# Table is aliased or it's being changed and thus is aliased.
|
||||||
change_map.get(alias, alias): (aliased or alias in change_map)
|
change_map.get(alias, alias): (aliased or alias in change_map)
|
||||||
for alias, aliased in self.external_aliases.items()
|
for alias, aliased in self.external_aliases.items()
|
||||||
}
|
}
|
||||||
|
for combined_query in self.combined_queries:
|
||||||
|
external_change_map = {
|
||||||
|
alias: aliased
|
||||||
|
for alias, aliased in change_map.items()
|
||||||
|
if alias in combined_query.external_aliases
|
||||||
|
}
|
||||||
|
combined_query.change_aliases(external_change_map)
|
||||||
|
|
||||||
def bump_prefix(self, other_query, exclude=None):
|
def bump_prefix(self, other_query, exclude=None):
|
||||||
"""
|
"""
|
||||||
|
@ -570,7 +570,12 @@ def formset_factory(
|
|||||||
"validate_max": validate_max,
|
"validate_max": validate_max,
|
||||||
"renderer": renderer,
|
"renderer": renderer,
|
||||||
}
|
}
|
||||||
return type(form.__name__ + "FormSet", (formset,), attrs)
|
form_name = form.__name__
|
||||||
|
if form_name.endswith("Form"):
|
||||||
|
formset_name = form_name + "Set"
|
||||||
|
else:
|
||||||
|
formset_name = form_name + "FormSet"
|
||||||
|
return type(formset_name, (formset,), attrs)
|
||||||
|
|
||||||
|
|
||||||
def all_valid(formsets):
|
def all_valid(formsets):
|
||||||
|
@ -21,6 +21,7 @@ from django.http.cookie import SimpleCookie
|
|||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.datastructures import CaseInsensitiveMapping
|
from django.utils.datastructures import CaseInsensitiveMapping
|
||||||
from django.utils.encoding import iri_to_uri
|
from django.utils.encoding import iri_to_uri
|
||||||
|
from django.utils.functional import cached_property
|
||||||
from django.utils.http import content_disposition_header, http_date
|
from django.utils.http import content_disposition_header, http_date
|
||||||
from django.utils.regex_helper import _lazy_re_compile
|
from django.utils.regex_helper import _lazy_re_compile
|
||||||
|
|
||||||
@ -408,6 +409,11 @@ class HttpResponse(HttpResponseBase):
|
|||||||
content = self.make_bytes(value)
|
content = self.make_bytes(value)
|
||||||
# Create a list of properly encoded bytestrings to support write().
|
# Create a list of properly encoded bytestrings to support write().
|
||||||
self._container = [content]
|
self._container = [content]
|
||||||
|
self.__dict__.pop("text", None)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def text(self):
|
||||||
|
return self.content.decode(self.charset or "utf-8")
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
return iter(self._container)
|
return iter(self._container)
|
||||||
@ -460,6 +466,12 @@ class StreamingHttpResponse(HttpResponseBase):
|
|||||||
"`streaming_content` instead." % self.__class__.__name__
|
"`streaming_content` instead." % self.__class__.__name__
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def text(self):
|
||||||
|
raise AttributeError(
|
||||||
|
"This %s instance has no `text` attribute." % self.__class__.__name__
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def streaming_content(self):
|
def streaming_content(self):
|
||||||
if self.is_async:
|
if self.is_async:
|
||||||
@ -615,10 +627,12 @@ class FileResponse(StreamingHttpResponse):
|
|||||||
class HttpResponseRedirectBase(HttpResponse):
|
class HttpResponseRedirectBase(HttpResponse):
|
||||||
allowed_schemes = ["http", "https", "ftp"]
|
allowed_schemes = ["http", "https", "ftp"]
|
||||||
|
|
||||||
def __init__(self, redirect_to, *args, **kwargs):
|
def __init__(self, redirect_to, preserve_request=False, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self["Location"] = iri_to_uri(redirect_to)
|
self["Location"] = iri_to_uri(redirect_to)
|
||||||
parsed = urlsplit(str(redirect_to))
|
parsed = urlsplit(str(redirect_to))
|
||||||
|
if preserve_request:
|
||||||
|
self.status_code = self.status_code_preserve_request
|
||||||
if parsed.scheme and parsed.scheme not in self.allowed_schemes:
|
if parsed.scheme and parsed.scheme not in self.allowed_schemes:
|
||||||
raise DisallowedRedirect(
|
raise DisallowedRedirect(
|
||||||
"Unsafe redirect to URL with protocol '%s'" % parsed.scheme
|
"Unsafe redirect to URL with protocol '%s'" % parsed.scheme
|
||||||
@ -640,10 +654,12 @@ class HttpResponseRedirectBase(HttpResponse):
|
|||||||
|
|
||||||
class HttpResponseRedirect(HttpResponseRedirectBase):
|
class HttpResponseRedirect(HttpResponseRedirectBase):
|
||||||
status_code = 302
|
status_code = 302
|
||||||
|
status_code_preserve_request = 307
|
||||||
|
|
||||||
|
|
||||||
class HttpResponsePermanentRedirect(HttpResponseRedirectBase):
|
class HttpResponsePermanentRedirect(HttpResponseRedirectBase):
|
||||||
status_code = 301
|
status_code = 301
|
||||||
|
status_code_preserve_request = 308
|
||||||
|
|
||||||
|
|
||||||
class HttpResponseNotModified(HttpResponse):
|
class HttpResponseNotModified(HttpResponse):
|
||||||
|
@ -26,7 +26,7 @@ def render(
|
|||||||
return HttpResponse(content, content_type, status)
|
return HttpResponse(content, content_type, status)
|
||||||
|
|
||||||
|
|
||||||
def redirect(to, *args, permanent=False, **kwargs):
|
def redirect(to, *args, permanent=False, preserve_request=False, **kwargs):
|
||||||
"""
|
"""
|
||||||
Return an HttpResponseRedirect to the appropriate URL for the arguments
|
Return an HttpResponseRedirect to the appropriate URL for the arguments
|
||||||
passed.
|
passed.
|
||||||
@ -40,13 +40,17 @@ def redirect(to, *args, permanent=False, **kwargs):
|
|||||||
|
|
||||||
* A URL, which will be used as-is for the redirect location.
|
* A URL, which will be used as-is for the redirect location.
|
||||||
|
|
||||||
Issues a temporary redirect by default; pass permanent=True to issue a
|
Issues a temporary redirect by default. Set permanent=True to issue a
|
||||||
permanent redirect.
|
permanent redirect. Set preserve_request=True to instruct the user agent
|
||||||
|
to preserve the original HTTP method and body when following the redirect.
|
||||||
"""
|
"""
|
||||||
redirect_class = (
|
redirect_class = (
|
||||||
HttpResponsePermanentRedirect if permanent else HttpResponseRedirect
|
HttpResponsePermanentRedirect if permanent else HttpResponseRedirect
|
||||||
)
|
)
|
||||||
return redirect_class(resolve_url(to, *args, **kwargs))
|
return redirect_class(
|
||||||
|
resolve_url(to, *args, **kwargs),
|
||||||
|
preserve_request=preserve_request,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _get_queryset(klass):
|
def _get_queryset(klass):
|
||||||
|
@ -57,7 +57,7 @@ from enum import Enum
|
|||||||
|
|
||||||
from django.template.context import BaseContext
|
from django.template.context import BaseContext
|
||||||
from django.utils.formats import localize
|
from django.utils.formats import localize
|
||||||
from django.utils.html import conditional_escape, escape
|
from django.utils.html import conditional_escape
|
||||||
from django.utils.regex_helper import _lazy_re_compile
|
from django.utils.regex_helper import _lazy_re_compile
|
||||||
from django.utils.safestring import SafeData, SafeString, mark_safe
|
from django.utils.safestring import SafeData, SafeString, mark_safe
|
||||||
from django.utils.text import get_text_list, smart_split, unescape_string_literal
|
from django.utils.text import get_text_list, smart_split, unescape_string_literal
|
||||||
@ -247,10 +247,10 @@ class Template:
|
|||||||
for num, next in enumerate(linebreak_iter(self.source)):
|
for num, next in enumerate(linebreak_iter(self.source)):
|
||||||
if start >= upto and end <= next:
|
if start >= upto and end <= next:
|
||||||
line = num
|
line = num
|
||||||
before = escape(self.source[upto:start])
|
before = self.source[upto:start]
|
||||||
during = escape(self.source[start:end])
|
during = self.source[start:end]
|
||||||
after = escape(self.source[end:next])
|
after = self.source[end:next]
|
||||||
source_lines.append((num, escape(self.source[upto:next])))
|
source_lines.append((num, self.source[upto:next]))
|
||||||
upto = next
|
upto = next
|
||||||
total = len(source_lines)
|
total = len(source_lines)
|
||||||
|
|
||||||
|
@ -37,7 +37,9 @@ class BaseContext:
|
|||||||
self.dicts.append(value)
|
self.dicts.append(value)
|
||||||
|
|
||||||
def __copy__(self):
|
def __copy__(self):
|
||||||
duplicate = copy(super())
|
duplicate = BaseContext()
|
||||||
|
duplicate.__class__ = self.__class__
|
||||||
|
duplicate.__dict__ = copy(self.__dict__)
|
||||||
duplicate.dicts = self.dicts[:]
|
duplicate.dicts = self.dicts[:]
|
||||||
return duplicate
|
return duplicate
|
||||||
|
|
||||||
|
@ -153,6 +153,90 @@ class Library:
|
|||||||
else:
|
else:
|
||||||
raise ValueError("Invalid arguments provided to simple_tag")
|
raise ValueError("Invalid arguments provided to simple_tag")
|
||||||
|
|
||||||
|
def simple_block_tag(self, func=None, takes_context=None, name=None, end_name=None):
|
||||||
|
"""
|
||||||
|
Register a callable as a compiled block template tag. Example:
|
||||||
|
|
||||||
|
@register.simple_block_tag
|
||||||
|
def hello(content):
|
||||||
|
return 'world'
|
||||||
|
"""
|
||||||
|
|
||||||
|
def dec(func):
|
||||||
|
nonlocal end_name
|
||||||
|
|
||||||
|
(
|
||||||
|
params,
|
||||||
|
varargs,
|
||||||
|
varkw,
|
||||||
|
defaults,
|
||||||
|
kwonly,
|
||||||
|
kwonly_defaults,
|
||||||
|
_,
|
||||||
|
) = getfullargspec(unwrap(func))
|
||||||
|
function_name = name or func.__name__
|
||||||
|
|
||||||
|
if end_name is None:
|
||||||
|
end_name = f"end{function_name}"
|
||||||
|
|
||||||
|
@wraps(func)
|
||||||
|
def compile_func(parser, token):
|
||||||
|
tag_params = params.copy()
|
||||||
|
|
||||||
|
if takes_context:
|
||||||
|
if len(tag_params) >= 2 and tag_params[1] == "content":
|
||||||
|
del tag_params[1]
|
||||||
|
else:
|
||||||
|
raise TemplateSyntaxError(
|
||||||
|
f"{function_name!r} is decorated with takes_context=True so"
|
||||||
|
" it must have a first argument of 'context' and a second "
|
||||||
|
"argument of 'content'"
|
||||||
|
)
|
||||||
|
elif tag_params and tag_params[0] == "content":
|
||||||
|
del tag_params[0]
|
||||||
|
else:
|
||||||
|
raise TemplateSyntaxError(
|
||||||
|
f"'{function_name}' must have a first argument of 'content'"
|
||||||
|
)
|
||||||
|
|
||||||
|
bits = token.split_contents()[1:]
|
||||||
|
target_var = None
|
||||||
|
if len(bits) >= 2 and bits[-2] == "as":
|
||||||
|
target_var = bits[-1]
|
||||||
|
bits = bits[:-2]
|
||||||
|
|
||||||
|
nodelist = parser.parse((end_name,))
|
||||||
|
parser.delete_first_token()
|
||||||
|
|
||||||
|
args, kwargs = parse_bits(
|
||||||
|
parser,
|
||||||
|
bits,
|
||||||
|
tag_params,
|
||||||
|
varargs,
|
||||||
|
varkw,
|
||||||
|
defaults,
|
||||||
|
kwonly,
|
||||||
|
kwonly_defaults,
|
||||||
|
takes_context,
|
||||||
|
function_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
return SimpleBlockNode(
|
||||||
|
nodelist, func, takes_context, args, kwargs, target_var
|
||||||
|
)
|
||||||
|
|
||||||
|
self.tag(function_name, compile_func)
|
||||||
|
return func
|
||||||
|
|
||||||
|
if func is None:
|
||||||
|
# @register.simple_block_tag(...)
|
||||||
|
return dec
|
||||||
|
elif callable(func):
|
||||||
|
# @register.simple_block_tag
|
||||||
|
return dec(func)
|
||||||
|
else:
|
||||||
|
raise ValueError("Invalid arguments provided to simple_block_tag")
|
||||||
|
|
||||||
def inclusion_tag(self, filename, func=None, takes_context=None, name=None):
|
def inclusion_tag(self, filename, func=None, takes_context=None, name=None):
|
||||||
"""
|
"""
|
||||||
Register a callable as an inclusion tag:
|
Register a callable as an inclusion tag:
|
||||||
@ -243,6 +327,23 @@ class SimpleNode(TagHelperNode):
|
|||||||
return output
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleBlockNode(SimpleNode):
|
||||||
|
def __init__(self, nodelist, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.nodelist = nodelist
|
||||||
|
|
||||||
|
def get_resolved_arguments(self, context):
|
||||||
|
resolved_args, resolved_kwargs = super().get_resolved_arguments(context)
|
||||||
|
|
||||||
|
# Restore the "content" argument.
|
||||||
|
# It will move depending on whether takes_context was passed.
|
||||||
|
resolved_args.insert(
|
||||||
|
1 if self.takes_context else 0, self.nodelist.render(context)
|
||||||
|
)
|
||||||
|
|
||||||
|
return resolved_args, resolved_kwargs
|
||||||
|
|
||||||
|
|
||||||
class InclusionNode(TagHelperNode):
|
class InclusionNode(TagHelperNode):
|
||||||
def __init__(self, func, takes_context, args, kwargs, filename):
|
def __init__(self, func, takes_context, args, kwargs, filename):
|
||||||
super().__init__(func, takes_context, args, kwargs)
|
super().__init__(func, takes_context, args, kwargs)
|
||||||
|
@ -947,9 +947,7 @@ class ClientMixin:
|
|||||||
'Content-Type header is "%s", not "application/json"'
|
'Content-Type header is "%s", not "application/json"'
|
||||||
% response.get("Content-Type")
|
% response.get("Content-Type")
|
||||||
)
|
)
|
||||||
response._json = json.loads(
|
response._json = json.loads(response.text, **extra)
|
||||||
response.content.decode(response.charset), **extra
|
|
||||||
)
|
|
||||||
return response._json
|
return response._json
|
||||||
|
|
||||||
def _follow_redirect(
|
def _follow_redirect(
|
||||||
|
@ -12,6 +12,7 @@ import random
|
|||||||
import sys
|
import sys
|
||||||
import textwrap
|
import textwrap
|
||||||
import unittest
|
import unittest
|
||||||
|
import unittest.suite
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
@ -292,7 +293,15 @@ failure and get a correct traceback.
|
|||||||
|
|
||||||
def addError(self, test, err):
|
def addError(self, test, err):
|
||||||
self.check_picklable(test, err)
|
self.check_picklable(test, err)
|
||||||
self.events.append(("addError", self.test_index, err))
|
|
||||||
|
event_occurred_before_first_test = self.test_index == -1
|
||||||
|
if event_occurred_before_first_test and isinstance(
|
||||||
|
test, unittest.suite._ErrorHolder
|
||||||
|
):
|
||||||
|
self.events.append(("addError", self.test_index, test.id(), err))
|
||||||
|
else:
|
||||||
|
self.events.append(("addError", self.test_index, err))
|
||||||
|
|
||||||
super().addError(test, err)
|
super().addError(test, err)
|
||||||
|
|
||||||
def addFailure(self, test, err):
|
def addFailure(self, test, err):
|
||||||
@ -547,18 +556,32 @@ class ParallelTestSuite(unittest.TestSuite):
|
|||||||
|
|
||||||
tests = list(self.subsuites[subsuite_index])
|
tests = list(self.subsuites[subsuite_index])
|
||||||
for event in events:
|
for event in events:
|
||||||
event_name = event[0]
|
self.handle_event(result, tests, event)
|
||||||
handler = getattr(result, event_name, None)
|
|
||||||
if handler is None:
|
|
||||||
continue
|
|
||||||
test = tests[event[1]]
|
|
||||||
args = event[2:]
|
|
||||||
handler(test, *args)
|
|
||||||
|
|
||||||
pool.join()
|
pool.join()
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def handle_event(self, result, tests, event):
|
||||||
|
event_name = event[0]
|
||||||
|
handler = getattr(result, event_name, None)
|
||||||
|
if handler is None:
|
||||||
|
return
|
||||||
|
test_index = event[1]
|
||||||
|
event_occurred_before_first_test = test_index == -1
|
||||||
|
if (
|
||||||
|
event_name == "addError"
|
||||||
|
and event_occurred_before_first_test
|
||||||
|
and len(event) >= 4
|
||||||
|
):
|
||||||
|
test_id = event[2]
|
||||||
|
test = unittest.suite._ErrorHolder(test_id)
|
||||||
|
args = event[3:]
|
||||||
|
else:
|
||||||
|
test = tests[test_index]
|
||||||
|
args = event[2:]
|
||||||
|
handler(test, *args)
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
return iter(self.subsuites)
|
return iter(self.subsuites)
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@ PY310 = sys.version_info >= (3, 10)
|
|||||||
PY311 = sys.version_info >= (3, 11)
|
PY311 = sys.version_info >= (3, 11)
|
||||||
PY312 = sys.version_info >= (3, 12)
|
PY312 = sys.version_info >= (3, 12)
|
||||||
PY313 = sys.version_info >= (3, 13)
|
PY313 = sys.version_info >= (3, 13)
|
||||||
|
PY314 = sys.version_info >= (3, 14)
|
||||||
|
|
||||||
|
|
||||||
def get_version(version=None):
|
def get_version(version=None):
|
||||||
|
@ -300,7 +300,11 @@ class DateMixin:
|
|||||||
|
|
||||||
|
|
||||||
class BaseDateListView(MultipleObjectMixin, DateMixin, View):
|
class BaseDateListView(MultipleObjectMixin, DateMixin, View):
|
||||||
"""Abstract base class for date-based views displaying a list of objects."""
|
"""
|
||||||
|
Base class for date-based views displaying a list of objects.
|
||||||
|
|
||||||
|
This requires subclassing to provide a response mixin.
|
||||||
|
"""
|
||||||
|
|
||||||
allow_empty = False
|
allow_empty = False
|
||||||
date_list_period = "year"
|
date_list_period = "year"
|
||||||
@ -388,7 +392,9 @@ class BaseDateListView(MultipleObjectMixin, DateMixin, View):
|
|||||||
|
|
||||||
class BaseArchiveIndexView(BaseDateListView):
|
class BaseArchiveIndexView(BaseDateListView):
|
||||||
"""
|
"""
|
||||||
Base class for archives of date-based items. Requires a response mixin.
|
Base view for archives of date-based items.
|
||||||
|
|
||||||
|
This requires subclassing to provide a response mixin.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
context_object_name = "latest"
|
context_object_name = "latest"
|
||||||
@ -411,7 +417,11 @@ class ArchiveIndexView(MultipleObjectTemplateResponseMixin, BaseArchiveIndexView
|
|||||||
|
|
||||||
|
|
||||||
class BaseYearArchiveView(YearMixin, BaseDateListView):
|
class BaseYearArchiveView(YearMixin, BaseDateListView):
|
||||||
"""List of objects published in a given year."""
|
"""
|
||||||
|
Base view for a list of objects published in a given year.
|
||||||
|
|
||||||
|
This requires subclassing to provide a response mixin.
|
||||||
|
"""
|
||||||
|
|
||||||
date_list_period = "month"
|
date_list_period = "month"
|
||||||
make_object_list = False
|
make_object_list = False
|
||||||
@ -463,7 +473,11 @@ class YearArchiveView(MultipleObjectTemplateResponseMixin, BaseYearArchiveView):
|
|||||||
|
|
||||||
|
|
||||||
class BaseMonthArchiveView(YearMixin, MonthMixin, BaseDateListView):
|
class BaseMonthArchiveView(YearMixin, MonthMixin, BaseDateListView):
|
||||||
"""List of objects published in a given month."""
|
"""
|
||||||
|
Base view for a list of objects published in a given month.
|
||||||
|
|
||||||
|
This requires subclassing to provide a response mixin.
|
||||||
|
"""
|
||||||
|
|
||||||
date_list_period = "day"
|
date_list_period = "day"
|
||||||
|
|
||||||
@ -505,7 +519,11 @@ class MonthArchiveView(MultipleObjectTemplateResponseMixin, BaseMonthArchiveView
|
|||||||
|
|
||||||
|
|
||||||
class BaseWeekArchiveView(YearMixin, WeekMixin, BaseDateListView):
|
class BaseWeekArchiveView(YearMixin, WeekMixin, BaseDateListView):
|
||||||
"""List of objects published in a given week."""
|
"""
|
||||||
|
Base view for a list of objects published in a given week.
|
||||||
|
|
||||||
|
This requires subclassing to provide a response mixin.
|
||||||
|
"""
|
||||||
|
|
||||||
def get_dated_items(self):
|
def get_dated_items(self):
|
||||||
"""Return (date_list, items, extra_context) for this request."""
|
"""Return (date_list, items, extra_context) for this request."""
|
||||||
@ -563,7 +581,11 @@ class WeekArchiveView(MultipleObjectTemplateResponseMixin, BaseWeekArchiveView):
|
|||||||
|
|
||||||
|
|
||||||
class BaseDayArchiveView(YearMixin, MonthMixin, DayMixin, BaseDateListView):
|
class BaseDayArchiveView(YearMixin, MonthMixin, DayMixin, BaseDateListView):
|
||||||
"""List of objects published on a given day."""
|
"""
|
||||||
|
Base view for a list of objects published on a given day.
|
||||||
|
|
||||||
|
This requires subclassing to provide a response mixin.
|
||||||
|
"""
|
||||||
|
|
||||||
def get_dated_items(self):
|
def get_dated_items(self):
|
||||||
"""Return (date_list, items, extra_context) for this request."""
|
"""Return (date_list, items, extra_context) for this request."""
|
||||||
@ -610,7 +632,11 @@ class DayArchiveView(MultipleObjectTemplateResponseMixin, BaseDayArchiveView):
|
|||||||
|
|
||||||
|
|
||||||
class BaseTodayArchiveView(BaseDayArchiveView):
|
class BaseTodayArchiveView(BaseDayArchiveView):
|
||||||
"""List of objects published today."""
|
"""
|
||||||
|
Base view for a list of objects published today.
|
||||||
|
|
||||||
|
This requires subclassing to provide a response mixin.
|
||||||
|
"""
|
||||||
|
|
||||||
def get_dated_items(self):
|
def get_dated_items(self):
|
||||||
"""Return (date_list, items, extra_context) for this request."""
|
"""Return (date_list, items, extra_context) for this request."""
|
||||||
@ -625,8 +651,10 @@ class TodayArchiveView(MultipleObjectTemplateResponseMixin, BaseTodayArchiveView
|
|||||||
|
|
||||||
class BaseDateDetailView(YearMixin, MonthMixin, DayMixin, DateMixin, BaseDetailView):
|
class BaseDateDetailView(YearMixin, MonthMixin, DayMixin, DateMixin, BaseDetailView):
|
||||||
"""
|
"""
|
||||||
Detail view of a single object on a single date; this differs from the
|
Base detail view for a single object on a single date; this differs from the
|
||||||
standard DetailView by accepting a year/month/day in the URL.
|
standard DetailView by accepting a year/month/day in the URL.
|
||||||
|
|
||||||
|
This requires subclassing to provide a response mixin.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get_object(self, queryset=None):
|
def get_object(self, queryset=None):
|
||||||
|
@ -102,7 +102,11 @@ class SingleObjectMixin(ContextMixin):
|
|||||||
|
|
||||||
|
|
||||||
class BaseDetailView(SingleObjectMixin, View):
|
class BaseDetailView(SingleObjectMixin, View):
|
||||||
"""A base view for displaying a single object."""
|
"""
|
||||||
|
Base view for displaying a single object.
|
||||||
|
|
||||||
|
This requires subclassing to provide a response mixin.
|
||||||
|
"""
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
self.object = self.get_object()
|
self.object = self.get_object()
|
||||||
|
@ -170,7 +170,7 @@ class BaseCreateView(ModelFormMixin, ProcessFormView):
|
|||||||
"""
|
"""
|
||||||
Base view for creating a new object instance.
|
Base view for creating a new object instance.
|
||||||
|
|
||||||
Using this base class requires subclassing to provide a response mixin.
|
This requires subclassing to provide a response mixin.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
@ -194,7 +194,7 @@ class BaseUpdateView(ModelFormMixin, ProcessFormView):
|
|||||||
"""
|
"""
|
||||||
Base view for updating an existing object.
|
Base view for updating an existing object.
|
||||||
|
|
||||||
Using this base class requires subclassing to provide a response mixin.
|
This requires subclassing to provide a response mixin.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
@ -242,7 +242,7 @@ class BaseDeleteView(DeletionMixin, FormMixin, BaseDetailView):
|
|||||||
"""
|
"""
|
||||||
Base view for deleting an object.
|
Base view for deleting an object.
|
||||||
|
|
||||||
Using this base class requires subclassing to provide a response mixin.
|
This requires subclassing to provide a response mixin.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
form_class = Form
|
form_class = Form
|
||||||
|
@ -148,7 +148,11 @@ class MultipleObjectMixin(ContextMixin):
|
|||||||
|
|
||||||
|
|
||||||
class BaseListView(MultipleObjectMixin, View):
|
class BaseListView(MultipleObjectMixin, View):
|
||||||
"""A base view for displaying a list of objects."""
|
"""
|
||||||
|
Base view for displaying a list of objects.
|
||||||
|
|
||||||
|
This requires subclassing to provide a response mixin.
|
||||||
|
"""
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
self.object_list = self.get_queryset()
|
self.object_list = self.get_queryset()
|
||||||
|
@ -212,7 +212,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% if frames %}
|
{% if frames %}
|
||||||
<div id="traceback">
|
<div id="traceback">
|
||||||
<h2>Traceback{% if not is_email %} <span class="commands"><a href="#" onclick="return switchPastebinFriendly(this);">
|
<h2>Traceback{% if not is_email %} <span class="commands"><a href="#" role="button" onclick="return switchPastebinFriendly(this);">
|
||||||
Switch to copy-and-paste view</a></span>{% endif %}
|
Switch to copy-and-paste view</a></span>{% endif %}
|
||||||
</h2>
|
</h2>
|
||||||
<div id="browserTraceback">
|
<div id="browserTraceback">
|
||||||
|
@ -8,6 +8,7 @@ SPHINXBUILD ?= sphinx-build
|
|||||||
PAPER ?=
|
PAPER ?=
|
||||||
BUILDDIR ?= _build
|
BUILDDIR ?= _build
|
||||||
LANGUAGE ?=
|
LANGUAGE ?=
|
||||||
|
JOBS ?= auto
|
||||||
|
|
||||||
# Set the default language.
|
# Set the default language.
|
||||||
ifndef LANGUAGE
|
ifndef LANGUAGE
|
||||||
@ -21,7 +22,7 @@ LANGUAGEOPT = $(firstword $(subst _, ,$(LANGUAGE)))
|
|||||||
# Internal variables.
|
# Internal variables.
|
||||||
PAPEROPT_a4 = -D latex_paper_size=a4
|
PAPEROPT_a4 = -D latex_paper_size=a4
|
||||||
PAPEROPT_letter = -D latex_paper_size=letter
|
PAPEROPT_letter = -D latex_paper_size=letter
|
||||||
ALLSPHINXOPTS = -n -d $(BUILDDIR)/doctrees -D language=$(LANGUAGEOPT) $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
ALLSPHINXOPTS = -n -d $(BUILDDIR)/doctrees -D language=$(LANGUAGEOPT) --jobs $(JOBS) $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||||
# the i18n builder cannot share the environment and doctrees with the others
|
# the i18n builder cannot share the environment and doctrees with the others
|
||||||
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||||
|
|
||||||
@ -61,7 +62,7 @@ html:
|
|||||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
||||||
|
|
||||||
htmlview: html
|
htmlview: html
|
||||||
$(PYTHON) -c "import webbrowser; webbrowser.open('_build/html/index.html')"
|
$(PYTHON) -m webbrowser "$(BUILDDIR)/html/index.html"
|
||||||
|
|
||||||
dirhtml:
|
dirhtml:
|
||||||
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
|
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
|
||||||
|
@ -13,6 +13,8 @@ import functools
|
|||||||
import sys
|
import sys
|
||||||
from os.path import abspath, dirname, join
|
from os.path import abspath, dirname, join
|
||||||
|
|
||||||
|
from sphinx import version_info as sphinx_version
|
||||||
|
|
||||||
# Workaround for sphinx-build recursion limit overflow:
|
# Workaround for sphinx-build recursion limit overflow:
|
||||||
# pickle.dump(doctree, f, pickle.HIGHEST_PROTOCOL)
|
# pickle.dump(doctree, f, pickle.HIGHEST_PROTOCOL)
|
||||||
# RuntimeError: maximum recursion depth exceeded while pickling an object
|
# RuntimeError: maximum recursion depth exceeded while pickling an object
|
||||||
@ -138,13 +140,15 @@ django_next_version = "5.2"
|
|||||||
extlinks = {
|
extlinks = {
|
||||||
"bpo": ("https://bugs.python.org/issue?@action=redirect&bpo=%s", "bpo-%s"),
|
"bpo": ("https://bugs.python.org/issue?@action=redirect&bpo=%s", "bpo-%s"),
|
||||||
"commit": ("https://github.com/django/django/commit/%s", "%s"),
|
"commit": ("https://github.com/django/django/commit/%s", "%s"),
|
||||||
"cve": ("https://nvd.nist.gov/vuln/detail/CVE-%s", "CVE-%s"),
|
|
||||||
"pypi": ("https://pypi.org/project/%s/", "%s"),
|
"pypi": ("https://pypi.org/project/%s/", "%s"),
|
||||||
# A file or directory. GitHub redirects from blob to tree if needed.
|
# A file or directory. GitHub redirects from blob to tree if needed.
|
||||||
"source": ("https://github.com/django/django/blob/main/%s", "%s"),
|
"source": ("https://github.com/django/django/blob/main/%s", "%s"),
|
||||||
"ticket": ("https://code.djangoproject.com/ticket/%s", "#%s"),
|
"ticket": ("https://code.djangoproject.com/ticket/%s", "#%s"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if sphinx_version < (8, 1):
|
||||||
|
extlinks["cve"] = ("https://www.cve.org/CVERecord?id=CVE-%s", "CVE-%s")
|
||||||
|
|
||||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||||
# for a list of supported languages.
|
# for a list of supported languages.
|
||||||
# language = None
|
# language = None
|
||||||
|
@ -53,8 +53,8 @@ To determine the right time, you need to keep an eye on the schedule. If you
|
|||||||
post your message right before a release deadline, you're not likely to get the
|
post your message right before a release deadline, you're not likely to get the
|
||||||
sort of attention you require.
|
sort of attention you require.
|
||||||
|
|
||||||
Gentle IRC reminders can also work -- again, strategically timed if possible.
|
Gentle reminders in the ``#contributing-getting-started`` channel in the
|
||||||
During a bug sprint would be a very good time, for example.
|
`Django Discord server`_ can work.
|
||||||
|
|
||||||
Another way to get traction is to pull several related tickets together. When
|
Another way to get traction is to pull several related tickets together. When
|
||||||
someone sits down to review a bug in an area they haven't touched for
|
someone sits down to review a bug in an area they haven't touched for
|
||||||
@ -68,6 +68,8 @@ issue over and over again. This sort of behavior will not gain you any
|
|||||||
additional attention -- certainly not the attention that you need in order to
|
additional attention -- certainly not the attention that you need in order to
|
||||||
get your issue addressed.
|
get your issue addressed.
|
||||||
|
|
||||||
|
.. _`Django Discord server`: https://discord.gg/xcRH6mN4fa
|
||||||
|
|
||||||
But I've reminded you several times and you keep ignoring my contribution!
|
But I've reminded you several times and you keep ignoring my contribution!
|
||||||
==========================================================================
|
==========================================================================
|
||||||
|
|
||||||
|
@ -6,12 +6,11 @@ This document describes how to make use of external authentication sources
|
|||||||
(where the web server sets the ``REMOTE_USER`` environment variable) in your
|
(where the web server sets the ``REMOTE_USER`` environment variable) in your
|
||||||
Django applications. This type of authentication solution is typically seen on
|
Django applications. This type of authentication solution is typically seen on
|
||||||
intranet sites, with single sign-on solutions such as IIS and Integrated
|
intranet sites, with single sign-on solutions such as IIS and Integrated
|
||||||
Windows Authentication or Apache and `mod_authnz_ldap`_, `CAS`_, `Cosign`_,
|
Windows Authentication or Apache and `mod_authnz_ldap`_, `CAS`_, `WebAuth`_,
|
||||||
`WebAuth`_, `mod_auth_sspi`_, etc.
|
`mod_auth_sspi`_, etc.
|
||||||
|
|
||||||
.. _mod_authnz_ldap: https://httpd.apache.org/docs/2.2/mod/mod_authnz_ldap.html
|
.. _mod_authnz_ldap: https://httpd.apache.org/docs/current/mod/mod_authnz_ldap.html
|
||||||
.. _CAS: https://www.apereo.org/projects/cas
|
.. _CAS: https://www.apereo.org/projects/cas
|
||||||
.. _Cosign: http://weblogin.org
|
|
||||||
.. _WebAuth: https://uit.stanford.edu/service/authentication
|
.. _WebAuth: https://uit.stanford.edu/service/authentication
|
||||||
.. _mod_auth_sspi: https://sourceforge.net/projects/mod-auth-sspi
|
.. _mod_auth_sspi: https://sourceforge.net/projects/mod-auth-sspi
|
||||||
|
|
||||||
|
@ -498,6 +498,195 @@ you see fit:
|
|||||||
{% current_time "%Y-%m-%d %I:%M %p" as the_time %}
|
{% current_time "%Y-%m-%d %I:%M %p" as the_time %}
|
||||||
<p>The time is {{ the_time }}.</p>
|
<p>The time is {{ the_time }}.</p>
|
||||||
|
|
||||||
|
.. _howto-custom-template-tags-simple-block-tags:
|
||||||
|
|
||||||
|
Simple block tags
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
.. versionadded:: 5.2
|
||||||
|
|
||||||
|
.. method:: django.template.Library.simple_block_tag()
|
||||||
|
|
||||||
|
When a section of rendered template needs to be passed into a custom tag,
|
||||||
|
Django provides the ``simple_block_tag`` helper function to accomplish this.
|
||||||
|
Similar to :meth:`~django.template.Library.simple_tag()`, this function accepts
|
||||||
|
a custom tag function, but with the additional ``content`` argument, which
|
||||||
|
contains the rendered content as defined inside the tag. This allows dynamic
|
||||||
|
template sections to be easily incorporated into custom tags.
|
||||||
|
|
||||||
|
For example, a custom block tag which creates a chart could look like this::
|
||||||
|
|
||||||
|
from django import template
|
||||||
|
from myapp.charts import render_chart
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_block_tag
|
||||||
|
def chart(content):
|
||||||
|
return render_chart(source=content)
|
||||||
|
|
||||||
|
The ``content`` argument contains everything in between the ``{% chart %}``
|
||||||
|
and ``{% endchart %}`` tags:
|
||||||
|
|
||||||
|
.. code-block:: html+django
|
||||||
|
|
||||||
|
{% chart %}
|
||||||
|
digraph G {
|
||||||
|
label = "Chart for {{ request.user }}"
|
||||||
|
A -> {B C}
|
||||||
|
}
|
||||||
|
{% endchart %}
|
||||||
|
|
||||||
|
If there are other template tags or variables inside the ``content`` block,
|
||||||
|
they will be rendered before being passed to the tag function. In the example
|
||||||
|
above, ``request.user`` will be resolved by the time ``render_chart`` is
|
||||||
|
called.
|
||||||
|
|
||||||
|
Block tags are closed with ``end{name}`` (for example, ``endchart``). This can
|
||||||
|
be customized with the ``end_name`` parameter::
|
||||||
|
|
||||||
|
@register.simple_block_tag(end_name="endofchart")
|
||||||
|
def chart(content):
|
||||||
|
return render_chart(source=content)
|
||||||
|
|
||||||
|
Which would require a template definition like this:
|
||||||
|
|
||||||
|
.. code-block:: html+django
|
||||||
|
|
||||||
|
{% chart %}
|
||||||
|
digraph G {
|
||||||
|
label = "Chart for {{ request.user }}"
|
||||||
|
A -> {B C}
|
||||||
|
}
|
||||||
|
{% endofchart %}
|
||||||
|
|
||||||
|
A few things to note about ``simple_block_tag``:
|
||||||
|
|
||||||
|
* The first argument must be called ``content``, and it will contain the
|
||||||
|
contents of the template tag as a rendered string.
|
||||||
|
* Variables passed to the tag are not included in the rendering context of the
|
||||||
|
content, as would be when using the ``{% with %}`` tag.
|
||||||
|
|
||||||
|
Just like :ref:`simple_tag<howto-custom-template-tags-simple-tags>`,
|
||||||
|
``simple_block_tag``:
|
||||||
|
|
||||||
|
* Validates the quantity and quality of the arguments.
|
||||||
|
* Strips quotes from arguments if necessary.
|
||||||
|
* Escapes the output accordingly.
|
||||||
|
* Supports passing ``takes_context=True`` at registration time to access
|
||||||
|
context. Note that in this case, the first argument to the custom function
|
||||||
|
*must* be called ``context``, and ``content`` must follow.
|
||||||
|
* Supports renaming the tag by passing the ``name`` argument when registering.
|
||||||
|
* Supports accepting any number of positional or keyword arguments.
|
||||||
|
* Supports storing the result in a template variable using the ``as`` variant.
|
||||||
|
|
||||||
|
.. admonition:: Content Escaping
|
||||||
|
|
||||||
|
``simple_block_tag`` behaves similarly to ``simple_tag`` regarding
|
||||||
|
auto-escaping. For details on escaping and safety, refer to ``simple_tag``.
|
||||||
|
Because the ``content`` argument has already been rendered by Django, it is
|
||||||
|
already escaped.
|
||||||
|
|
||||||
|
A complete example
|
||||||
|
~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Consider a custom template tag that generates a message box that supports
|
||||||
|
multiple message levels and content beyond a simple phrase. This could be
|
||||||
|
implemented using a ``simple_block_tag`` as follows:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
:caption: ``testapp/templatetags/testapptags.py``
|
||||||
|
|
||||||
|
from django import template
|
||||||
|
from django.utils.html import format_html
|
||||||
|
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_block_tag(takes_context=True)
|
||||||
|
def msgbox(context, content, level):
|
||||||
|
format_kwargs = {
|
||||||
|
"level": level.lower(),
|
||||||
|
"level_title": level.capitalize(),
|
||||||
|
"content": content,
|
||||||
|
"open": " open" if level.lower() == "error" else "",
|
||||||
|
"site": context.get("site", "My Site"),
|
||||||
|
}
|
||||||
|
result = """
|
||||||
|
<div class="msgbox {level}">
|
||||||
|
<details{open}>
|
||||||
|
<summary>
|
||||||
|
<strong>{level_title}</strong>: Please read for <i>{site}</i>
|
||||||
|
</summary>
|
||||||
|
<p>
|
||||||
|
{content}
|
||||||
|
</p>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
return format_html(result, **format_kwargs)
|
||||||
|
|
||||||
|
When combined with a minimal view and corresponding template, as shown here:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
:caption: ``testapp/views.py``
|
||||||
|
|
||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
|
||||||
|
def simpleblocktag_view(request):
|
||||||
|
return render(request, "test.html", context={"site": "Important Site"})
|
||||||
|
|
||||||
|
|
||||||
|
.. code-block:: html+django
|
||||||
|
:caption: ``testapp/templates/test.html``
|
||||||
|
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% load testapptags %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
{% msgbox level="error" %}
|
||||||
|
Please fix all errors. Further documentation can be found at
|
||||||
|
<a href="http://example.com">Docs</a>.
|
||||||
|
{% endmsgbox %}
|
||||||
|
|
||||||
|
{% msgbox level="info" %}
|
||||||
|
More information at: <a href="http://othersite.com">Other Site</a>/
|
||||||
|
{% endmsgbox %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
The following HTML is produced as the rendered output:
|
||||||
|
|
||||||
|
.. code-block:: html
|
||||||
|
|
||||||
|
<div class="msgbox error">
|
||||||
|
<details open>
|
||||||
|
<summary>
|
||||||
|
<strong>Error</strong>: Please read for <i>Important Site</i>
|
||||||
|
</summary>
|
||||||
|
<p>
|
||||||
|
Please fix all errors. Further documentation can be found at
|
||||||
|
<a href="http://example.com">Docs</a>.
|
||||||
|
</p>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="msgbox info">
|
||||||
|
<details>
|
||||||
|
<summary>
|
||||||
|
<strong>Info</strong>: Please read for <i>Important Site</i>
|
||||||
|
</summary>
|
||||||
|
<p>
|
||||||
|
More information at: <a href="http://othersite.com">Other Site</a>
|
||||||
|
</p>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
|
||||||
.. _howto-custom-template-tags-inclusion-tags:
|
.. _howto-custom-template-tags-inclusion-tags:
|
||||||
|
|
||||||
Inclusion tags
|
Inclusion tags
|
||||||
|
@ -17,7 +17,7 @@ You can install Hypercorn with ``pip``:
|
|||||||
Running Django in Hypercorn
|
Running Django in Hypercorn
|
||||||
===========================
|
===========================
|
||||||
|
|
||||||
When Hypercorn is installed, a ``hypercorn`` command is available
|
When :pypi:`Hypercorn` is installed, a ``hypercorn`` command is available
|
||||||
which runs ASGI applications. Hypercorn needs to be called with the
|
which runs ASGI applications. Hypercorn needs to be called with the
|
||||||
location of a module containing an ASGI application object, followed
|
location of a module containing an ASGI application object, followed
|
||||||
by what the application is called (separated by a colon).
|
by what the application is called (separated by a colon).
|
||||||
@ -35,4 +35,4 @@ this command from the same directory as your ``manage.py`` file.
|
|||||||
For more advanced usage, please read the `Hypercorn documentation
|
For more advanced usage, please read the `Hypercorn documentation
|
||||||
<Hypercorn_>`_.
|
<Hypercorn_>`_.
|
||||||
|
|
||||||
.. _Hypercorn: https://pgjones.gitlab.io/hypercorn/
|
.. _Hypercorn: https://hypercorn.readthedocs.io/
|
||||||
|
@ -1,11 +1,57 @@
|
|||||||
===============
|
=============
|
||||||
"How-to" guides
|
How-to guides
|
||||||
===============
|
=============
|
||||||
|
|
||||||
Here you'll find short answers to "How do I....?" types of questions. These
|
Practical guides covering common tasks and problems.
|
||||||
how-to guides don't cover topics in depth -- you'll find that material in the
|
|
||||||
:doc:`/topics/index` and the :doc:`/ref/index`. However, these guides will help
|
Models, data and databases
|
||||||
you quickly accomplish common tasks.
|
==========================
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 1
|
||||||
|
|
||||||
|
initial-data
|
||||||
|
legacy-databases
|
||||||
|
custom-model-fields
|
||||||
|
writing-migrations
|
||||||
|
custom-lookups
|
||||||
|
|
||||||
|
Templates and output
|
||||||
|
====================
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 1
|
||||||
|
|
||||||
|
outputting-csv
|
||||||
|
outputting-pdf
|
||||||
|
overriding-templates
|
||||||
|
custom-template-backend
|
||||||
|
custom-template-tags
|
||||||
|
|
||||||
|
Project configuration and management
|
||||||
|
====================================
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 1
|
||||||
|
|
||||||
|
static-files/index
|
||||||
|
logging
|
||||||
|
error-reporting
|
||||||
|
delete-app
|
||||||
|
|
||||||
|
Installing, deploying and upgrading
|
||||||
|
===================================
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 1
|
||||||
|
|
||||||
|
upgrade-version
|
||||||
|
windows
|
||||||
|
deployment/index
|
||||||
|
static-files/deployment
|
||||||
|
|
||||||
|
Other guides
|
||||||
|
============
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 1
|
:maxdepth: 1
|
||||||
@ -13,25 +59,7 @@ you quickly accomplish common tasks.
|
|||||||
auth-remote-user
|
auth-remote-user
|
||||||
csrf
|
csrf
|
||||||
custom-management-commands
|
custom-management-commands
|
||||||
custom-model-fields
|
|
||||||
custom-lookups
|
|
||||||
custom-template-backend
|
|
||||||
custom-template-tags
|
|
||||||
custom-file-storage
|
custom-file-storage
|
||||||
deployment/index
|
|
||||||
upgrade-version
|
|
||||||
error-reporting
|
|
||||||
initial-data
|
|
||||||
legacy-databases
|
|
||||||
logging
|
|
||||||
outputting-csv
|
|
||||||
outputting-pdf
|
|
||||||
overriding-templates
|
|
||||||
static-files/index
|
|
||||||
static-files/deployment
|
|
||||||
windows
|
|
||||||
writing-migrations
|
|
||||||
delete-app
|
|
||||||
|
|
||||||
.. seealso::
|
.. seealso::
|
||||||
|
|
||||||
|
@ -111,15 +111,15 @@ reimplement the entire template.
|
|||||||
For example, you can use this technique to add a custom logo to the
|
For example, you can use this technique to add a custom logo to the
|
||||||
``admin/base_site.html`` template:
|
``admin/base_site.html`` template:
|
||||||
|
|
||||||
.. code-block:: html+django
|
.. code-block:: html+django
|
||||||
:caption: ``templates/admin/base_site.html``
|
:caption: ``templates/admin/base_site.html``
|
||||||
|
|
||||||
{% extends "admin/base_site.html" %}
|
{% extends "admin/base_site.html" %}
|
||||||
|
|
||||||
{% block branding %}
|
{% block branding %}
|
||||||
<img src="link/to/logo.png" alt="logo">
|
<img src="link/to/logo.png" alt="logo">
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
Key points to note:
|
Key points to note:
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ Serving static files in production
|
|||||||
The basic outline of putting static files into production consists of two
|
The basic outline of putting static files into production consists of two
|
||||||
steps: run the :djadmin:`collectstatic` command when static files change, then
|
steps: run the :djadmin:`collectstatic` command when static files change, then
|
||||||
arrange for the collected static files directory (:setting:`STATIC_ROOT`) to be
|
arrange for the collected static files directory (:setting:`STATIC_ROOT`) to be
|
||||||
moved to the static file server and served. Depending the ``staticfiles``
|
moved to the static file server and served. Depending on the ``staticfiles``
|
||||||
:setting:`STORAGES` alias, files may need to be moved to a new location
|
:setting:`STORAGES` alias, files may need to be moved to a new location
|
||||||
manually or the :func:`post_process
|
manually or the :func:`post_process
|
||||||
<django.contrib.staticfiles.storage.StaticFilesStorage.post_process>` method of
|
<django.contrib.staticfiles.storage.StaticFilesStorage.post_process>` method of
|
||||||
|
@ -232,47 +232,47 @@
|
|||||||
</g>
|
</g>
|
||||||
<g id="Graphic_89">
|
<g id="Graphic_89">
|
||||||
<rect x="189" y="144" width="243" height="54" fill="white"/>
|
<rect x="189" y="144" width="243" height="54" fill="white"/>
|
||||||
<path d="M 432 198 L 189 198 L 189 144 L 432 144 Z" stroke="#aaa" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="4.0,4.0" stroke-width="1"/>
|
<path d="M 432 198 L 189 198 L 189 144 L 432 144 Z" stroke="#595959" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="4.0,4.0" stroke-width="1"/>
|
||||||
<text transform="translate(193 150)" fill="#797979">
|
<text transform="translate(193 150)" fill="#595959">
|
||||||
<tspan font-family="Helvetica" font-size="12" fill="#797979" x="19.789062" y="11">The ticket was already reported, was </tspan>
|
<tspan font-family="Helvetica" font-size="12" fill="#595959" x="19.789062" y="11">The ticket was already reported, was </tspan>
|
||||||
<tspan font-family="Helvetica" font-size="12" fill="#797979" x=".8017578" y="25">already rejected, isn't a bug, doesn't contain </tspan>
|
<tspan font-family="Helvetica" font-size="12" fill="#595959" x=".8017578" y="25">already rejected, isn't a bug, doesn't contain </tspan>
|
||||||
<tspan font-family="Helvetica" font-size="12" fill="#797979" x="1.2792969" y="39">enough information, or can't be reproduced.</tspan>
|
<tspan font-family="Helvetica" font-size="12" fill="#595959" x="1.2792969" y="39">enough information, or can't be reproduced.</tspan>
|
||||||
</text>
|
</text>
|
||||||
</g>
|
</g>
|
||||||
<g id="Line_90">
|
<g id="Line_90">
|
||||||
<line x1="252" y1="278.5" x2="252" y2="198" stroke="#aaa" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="4.0,4.0" stroke-width="1"/>
|
<line x1="252" y1="278.5" x2="252" y2="198" stroke="#595959" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="4.0,4.0" stroke-width="1"/>
|
||||||
</g>
|
</g>
|
||||||
<g id="Graphic_91">
|
<g id="Graphic_91">
|
||||||
<path d="M 258.36395 281.63605 C 261.8787 285.15076 261.8787 290.84924 258.36395 294.36395 C 254.84924 297.8787 249.15076 297.8787 245.63605 294.36395 C 242.1213 290.84924 242.1213 285.15076 245.63605 281.63605 C 249.15076 278.1213 254.84924 278.1213 258.36395 281.63605" stroke="#aaa" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="4.0,4.0" stroke-width="1"/>
|
<path d="M 258.36395 281.63605 C 261.8787 285.15076 261.8787 290.84924 258.36395 294.36395 C 254.84924 297.8787 249.15076 297.8787 245.63605 294.36395 C 242.1213 290.84924 242.1213 285.15076 245.63605 281.63605 C 249.15076 278.1213 254.84924 278.1213 258.36395 281.63605" stroke="#595959" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="4.0,4.0" stroke-width="1"/>
|
||||||
</g>
|
</g>
|
||||||
<g id="Graphic_96">
|
<g id="Graphic_96">
|
||||||
<rect x="72" y="144" width="99" height="54" fill="white"/>
|
<rect x="72" y="144" width="99" height="54" fill="white"/>
|
||||||
<path d="M 171 198 L 72 198 L 72 144 L 171 144 Z" stroke="#aaa" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="4.0,4.0" stroke-width="1"/>
|
<path d="M 171 198 L 72 198 L 72 144 L 171 144 Z" stroke="#595959" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="4.0,4.0" stroke-width="1"/>
|
||||||
<text transform="translate(76 150)" fill="#797979">
|
<text transform="translate(76 150)" fill="#595959">
|
||||||
<tspan font-family="Helvetica" font-size="12" fill="#797979" x="8.486328" y="11">The ticket is a </tspan>
|
<tspan font-family="Helvetica" font-size="12" fill="#595959" x="8.486328" y="11">The ticket is a </tspan>
|
||||||
<tspan font-family="Helvetica" font-size="12" fill="#797979" x="4.463867" y="25">bug and should </tspan>
|
<tspan font-family="Helvetica" font-size="12" fill="#595959" x="4.463867" y="25">bug and should </tspan>
|
||||||
<tspan font-family="Helvetica" font-size="12" fill="#797979" x="22.81836" y="39">be fixed.</tspan>
|
<tspan font-family="Helvetica" font-size="12" fill="#595959" x="22.81836" y="39">be fixed.</tspan>
|
||||||
</text>
|
</text>
|
||||||
</g>
|
</g>
|
||||||
<g id="Graphic_97">
|
<g id="Graphic_97">
|
||||||
<path d="M 150.36395 317.63605 C 153.87869 321.15076 153.87869 326.84924 150.36395 330.36395 C 146.84924 333.8787 141.15076 333.8787 137.63605 330.36395 C 134.12131 326.84924 134.12131 321.15076 137.63605 317.63605 C 141.15076 314.1213 146.84924 314.1213 150.36395 317.63605" stroke="#aaa" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="4.0,4.0" stroke-width="1"/>
|
<path d="M 150.36395 317.63605 C 153.87869 321.15076 153.87869 326.84924 150.36395 330.36395 C 146.84924 333.8787 141.15076 333.8787 137.63605 330.36395 C 134.12131 326.84924 134.12131 321.15076 137.63605 317.63605 C 141.15076 314.1213 146.84924 314.1213 150.36395 317.63605" stroke="#595959" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="4.0,4.0" stroke-width="1"/>
|
||||||
</g>
|
</g>
|
||||||
<g id="Line_98">
|
<g id="Line_98">
|
||||||
<path d="M 134.5 324 L 81 324 L 81 198" stroke="#aaa" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="4.0,4.0" stroke-width="1"/>
|
<path d="M 134.5 324 L 81 324 L 81 198" stroke="#595959" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="4.0,4.0" stroke-width="1"/>
|
||||||
</g>
|
</g>
|
||||||
<g id="Graphic_102">
|
<g id="Graphic_102">
|
||||||
<rect x="72" y="522" width="342" height="36" fill="white"/>
|
<rect x="72" y="522" width="342" height="36" fill="white"/>
|
||||||
<path d="M 414 558 L 72 558 L 72 522 L 414 522 Z" stroke="#aaa" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="4.0,4.0" stroke-width="1"/>
|
<path d="M 414 558 L 72 558 L 72 522 L 414 522 Z" stroke="#595959" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="4.0,4.0" stroke-width="1"/>
|
||||||
<text transform="translate(76 526)" fill="#797979">
|
<text transform="translate(76 526)" fill="#595959">
|
||||||
<tspan font-family="Helvetica" font-size="12" fill="#797979" x="7.241211" y="11">The ticket has a patch which applies cleanly and includes all </tspan>
|
<tspan font-family="Helvetica" font-size="12" fill="#595959" x="7.241211" y="11">The ticket has a patch which applies cleanly and includes all </tspan>
|
||||||
<tspan font-family="Helvetica" font-size="12" fill="#797979" x="26.591797" y="25">needed tests and docs. A merger can commit it as is.</tspan>
|
<tspan font-family="Helvetica" font-size="12" fill="#595959" x="26.591797" y="25">needed tests and docs. A merger can commit it as is.</tspan>
|
||||||
</text>
|
</text>
|
||||||
</g>
|
</g>
|
||||||
<g id="Graphic_103">
|
<g id="Graphic_103">
|
||||||
<path d="M 150.36395 407.63605 C 153.87869 411.15076 153.87869 416.84924 150.36395 420.36395 C 146.84924 423.8787 141.15076 423.8787 137.63605 420.36395 C 134.12131 416.84924 134.12131 411.15076 137.63605 407.63605 C 141.15076 404.1213 146.84924 404.1213 150.36395 407.63605" stroke="#aaa" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="4.0,4.0" stroke-width="1"/>
|
<path d="M 150.36395 407.63605 C 153.87869 411.15076 153.87869 416.84924 150.36395 420.36395 C 146.84924 423.8787 141.15076 423.8787 137.63605 420.36395 C 134.12131 416.84924 134.12131 411.15076 137.63605 407.63605 C 141.15076 404.1213 146.84924 404.1213 150.36395 407.63605" stroke="#595959" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="4.0,4.0" stroke-width="1"/>
|
||||||
</g>
|
</g>
|
||||||
<g id="Line_104">
|
<g id="Line_104">
|
||||||
<path d="M 134.5 414 L 81 414 L 81 522" stroke="#aaa" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="4.0,4.0" stroke-width="1"/>
|
<path d="M 134.5 414 L 81 414 L 81 522" stroke="#595959" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="4.0,4.0" stroke-width="1"/>
|
||||||
</g>
|
</g>
|
||||||
<g id="Line_151">
|
<g id="Line_151">
|
||||||
<line x1="252" y1="288" x2="303.79966" y2="317.5998" marker-end="url(#FilledArrow_Marker)" stroke="#008f00" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
<line x1="252" y1="288" x2="303.79966" y2="317.5998" marker-end="url(#FilledArrow_Marker)" stroke="#008f00" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||||
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
@ -46,7 +46,6 @@ a great ecosystem to work in:
|
|||||||
|
|
||||||
.. _posting guidelines: https://code.djangoproject.com/wiki/UsingTheMailingList
|
.. _posting guidelines: https://code.djangoproject.com/wiki/UsingTheMailingList
|
||||||
.. _#django IRC channel: https://web.libera.chat/#django
|
.. _#django IRC channel: https://web.libera.chat/#django
|
||||||
.. _#django-dev IRC channel: https://web.libera.chat/#django-dev
|
|
||||||
.. _community page: https://www.djangoproject.com/community/
|
.. _community page: https://www.djangoproject.com/community/
|
||||||
.. _Django Discord server: https://discord.gg/xcRH6mN4fa
|
.. _Django Discord server: https://discord.gg/xcRH6mN4fa
|
||||||
.. _Django forum: https://forum.djangoproject.com/
|
.. _Django forum: https://forum.djangoproject.com/
|
||||||
|
@ -21,53 +21,55 @@ First steps
|
|||||||
|
|
||||||
Start with these steps to discover Django's development process.
|
Start with these steps to discover Django's development process.
|
||||||
|
|
||||||
* **Triage tickets**
|
Triage tickets
|
||||||
|
--------------
|
||||||
|
|
||||||
If an `unreviewed ticket`_ reports a bug, try and reproduce it. If you
|
If an `unreviewed ticket`_ reports a bug, try and reproduce it. If you can
|
||||||
can reproduce it and it seems valid, make a note that you confirmed the bug
|
reproduce it and it seems valid, make a note that you confirmed the bug and
|
||||||
and accept the ticket. Make sure the ticket is filed under the correct
|
accept the ticket. Make sure the ticket is filed under the correct component
|
||||||
component area. Consider writing a patch that adds a test for the bug's
|
area. Consider writing a patch that adds a test for the bug's behavior, even if
|
||||||
behavior, even if you don't fix the bug itself. See more at
|
you don't fix the bug itself. See more at :ref:`how-can-i-help-with-triaging`
|
||||||
:ref:`how-can-i-help-with-triaging`
|
|
||||||
|
|
||||||
* **Look for tickets that are accepted and review patches to build familiarity
|
Review patches of accepted tickets
|
||||||
with the codebase and the process**
|
----------------------------------
|
||||||
|
|
||||||
Mark the appropriate flags if a patch needs docs or tests. Look through the
|
This will help you build familiarity with the codebase and processes. Mark the
|
||||||
changes a patch makes, and keep an eye out for syntax that is incompatible
|
appropriate flags if a patch needs docs or tests. Look through the changes a
|
||||||
with older but still supported versions of Python. :doc:`Run the tests
|
patch makes, and keep an eye out for syntax that is incompatible with older but
|
||||||
</internals/contributing/writing-code/unit-tests>` and make sure they pass.
|
still supported versions of Python. :doc:`Run the tests
|
||||||
Where possible and relevant, try them out on a database other than SQLite.
|
</internals/contributing/writing-code/unit-tests>` and make sure they pass.
|
||||||
Leave comments and feedback!
|
Where possible and relevant, try them out on a database other than SQLite.
|
||||||
|
Leave comments and feedback!
|
||||||
|
|
||||||
* **Keep old patches up to date**
|
Keep old patches up-to-date
|
||||||
|
---------------------------
|
||||||
|
|
||||||
Oftentimes the codebase will change between a patch being submitted and the
|
Oftentimes the codebase will change between a patch being submitted and the
|
||||||
time it gets reviewed. Make sure it still applies cleanly and functions as
|
time it gets reviewed. Make sure it still applies cleanly and functions as
|
||||||
expected. Updating a patch is both useful and important! See more on
|
expected. Updating a patch is both useful and important! See more on
|
||||||
:doc:`writing-code/submitting-patches`.
|
:doc:`writing-code/submitting-patches`.
|
||||||
|
|
||||||
* **Write some documentation**
|
Write some documentation
|
||||||
|
------------------------
|
||||||
|
|
||||||
Django's documentation is great but it can always be improved. Did you find
|
Django's documentation is great but it can always be improved. Did you find a
|
||||||
a typo? Do you think that something should be clarified? Go ahead and
|
typo? Do you think that something should be clarified? Go ahead and suggest a
|
||||||
suggest a documentation patch! See also the guide on
|
documentation patch! See also the guide on :doc:`writing-documentation`.
|
||||||
:doc:`writing-documentation`.
|
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
The `reports page`_ contains links to many useful Trac queries, including
|
The `reports page`_ contains links to many useful Trac queries, including
|
||||||
several that are useful for triaging tickets and reviewing patches as
|
several that are useful for triaging tickets and reviewing patches as
|
||||||
suggested above.
|
suggested above.
|
||||||
|
|
||||||
.. _reports page: https://code.djangoproject.com/wiki/Reports
|
.. _reports page: https://code.djangoproject.com/wiki/Reports
|
||||||
|
|
||||||
* **Sign the Contributor License Agreement**
|
Sign the Contributor License Agreement
|
||||||
|
--------------------------------------
|
||||||
|
|
||||||
The code that you write belongs to you or your employer. If your
|
The code that you write belongs to you or your employer. If your contribution
|
||||||
contribution is more than one or two lines of code, you need to sign the
|
is more than one or two lines of code, you need to sign the `CLA`_. See the
|
||||||
`CLA`_. See the `Contributor License Agreement FAQ`_ for a more thorough
|
`Contributor License Agreement FAQ`_ for a more thorough explanation.
|
||||||
explanation.
|
|
||||||
|
|
||||||
.. _CLA: https://www.djangoproject.com/foundation/cla/
|
.. _CLA: https://www.djangoproject.com/foundation/cla/
|
||||||
.. _Contributor License Agreement FAQ: https://www.djangoproject.com/foundation/cla/faq/
|
.. _Contributor License Agreement FAQ: https://www.djangoproject.com/foundation/cla/faq/
|
||||||
@ -80,78 +82,89 @@ Guidelines
|
|||||||
As a newcomer on a large project, it's easy to experience frustration. Here's
|
As a newcomer on a large project, it's easy to experience frustration. Here's
|
||||||
some advice to make your work on Django more useful and rewarding.
|
some advice to make your work on Django more useful and rewarding.
|
||||||
|
|
||||||
* **Pick a subject area that you care about, that you are familiar with, or
|
Pick a subject area
|
||||||
that you want to learn about**
|
-------------------
|
||||||
|
|
||||||
You don't already have to be an expert on the area you want to work on; you
|
This should be something that you care about, that you are familiar with or
|
||||||
become an expert through your ongoing contributions to the code.
|
that you want to learn about. You don't already have to be an expert on the
|
||||||
|
area you want to work on; you become an expert through your ongoing
|
||||||
|
contributions to the code.
|
||||||
|
|
||||||
* **Analyze tickets' context and history**
|
Analyze tickets' context and history
|
||||||
|
------------------------------------
|
||||||
|
|
||||||
Trac isn't an absolute; the context is just as important as the words.
|
Trac isn't an absolute; the context is just as important as the words. When
|
||||||
When reading Trac, you need to take into account who says things, and when
|
reading Trac, you need to take into account who says things, and when they were
|
||||||
they were said. Support for an idea two years ago doesn't necessarily mean
|
said. Support for an idea two years ago doesn't necessarily mean that the idea
|
||||||
that the idea will still have support. You also need to pay attention to who
|
will still have support. You also need to pay attention to who *hasn't* spoken
|
||||||
*hasn't* spoken -- for example, if an experienced contributor hasn't been
|
-- for example, if an experienced contributor hasn't been recently involved in
|
||||||
recently involved in a discussion, then a ticket may not have the support
|
a discussion, then a ticket may not have the support required to get into
|
||||||
required to get into Django.
|
Django.
|
||||||
|
|
||||||
* **Start small**
|
Start small
|
||||||
|
-----------
|
||||||
|
|
||||||
It's easier to get feedback on a little issue than on a big one. See the
|
It's easier to get feedback on a little issue than on a big one. See the
|
||||||
`easy pickings`_.
|
`easy pickings`_.
|
||||||
|
|
||||||
* **If you're going to engage in a big task, make sure that your idea has
|
Confirm support before engaging in a big task
|
||||||
support first**
|
---------------------------------------------
|
||||||
|
|
||||||
This means getting someone else to confirm that a bug is real before you fix
|
This means getting someone else to confirm that a bug is real before you fix
|
||||||
the issue, and ensuring that there's consensus on a proposed feature before
|
the issue, and ensuring that there's consensus on a proposed feature before you
|
||||||
you go implementing it.
|
go implementing it.
|
||||||
|
|
||||||
* **Be bold! Leave feedback!**
|
Be bold! Leave feedback!
|
||||||
|
------------------------
|
||||||
|
|
||||||
Sometimes it can be scary to put your opinion out to the world and say "this
|
Sometimes it can be scary to put your opinion out to the world and say "this
|
||||||
ticket is correct" or "this patch needs work", but it's the only way the
|
ticket is correct" or "this patch needs work", but it's the only way the
|
||||||
project moves forward. The contributions of the broad Django community
|
project moves forward. The contributions of the broad Django community
|
||||||
ultimately have a much greater impact than that of any one person. We can't
|
ultimately have a much greater impact than that of any one person. We can't do
|
||||||
do it without **you**!
|
it without **you**!
|
||||||
|
|
||||||
* **Err on the side of caution when marking things Ready For Check-in**
|
Be cautious when marking things "Ready For Check-in"
|
||||||
|
----------------------------------------------------
|
||||||
|
|
||||||
If you're really not certain if a ticket is ready, don't mark it as
|
If you're really not certain if a ticket is ready, don't mark it as such. Leave
|
||||||
such. Leave a comment instead, letting others know your thoughts. If you're
|
a comment instead, letting others know your thoughts. If you're mostly certain,
|
||||||
mostly certain, but not completely certain, you might also try asking on IRC
|
but not completely certain, you might also try asking on the
|
||||||
to see if someone else can confirm your suspicions.
|
``#contributing-getting-started`` channel in the `Django Discord server`_ to
|
||||||
|
see if someone else can confirm your suspicions.
|
||||||
|
|
||||||
* **Wait for feedback, and respond to feedback that you receive**
|
.. _`Django Discord server`: https://discord.gg/xcRH6mN4fa
|
||||||
|
|
||||||
Focus on one or two tickets, see them through from start to finish, and
|
Wait for feedback, and respond to feedback that you receive
|
||||||
repeat. The shotgun approach of taking on lots of tickets and letting some
|
-----------------------------------------------------------
|
||||||
fall by the wayside ends up doing more harm than good.
|
|
||||||
|
|
||||||
* **Be rigorous**
|
Focus on one or two tickets, see them through from start to finish, and repeat.
|
||||||
|
The shotgun approach of taking on lots of tickets and letting some fall by the
|
||||||
|
wayside ends up doing more harm than good.
|
||||||
|
|
||||||
When we say ":pep:`8`, and must have docs and tests", we mean it. If a patch
|
Be rigorous
|
||||||
doesn't have docs and tests, there had better be a good reason. Arguments
|
-----------
|
||||||
like "I couldn't find any existing tests of this feature" don't carry much
|
|
||||||
weight--while it may be true, that means you have the extra-important job of
|
|
||||||
writing the very first tests for that feature, not that you get a pass from
|
|
||||||
writing tests altogether.
|
|
||||||
|
|
||||||
* **Be patient**
|
When we say ":pep:`8`, and must have docs and tests", we mean it. If a patch
|
||||||
|
doesn't have docs and tests, there had better be a good reason. Arguments like
|
||||||
|
"I couldn't find any existing tests of this feature" don't carry much weight.
|
||||||
|
While it may be true, that means you have the extra-important job of writing
|
||||||
|
the very first tests for that feature, not that you get a pass from writing
|
||||||
|
tests altogether.
|
||||||
|
|
||||||
It's not always easy for your ticket or your patch to be reviewed quickly.
|
Be patient
|
||||||
This isn't personal. There are a lot of tickets and pull requests to get
|
----------
|
||||||
through.
|
|
||||||
|
|
||||||
Keeping your patch up to date is important. Review the ticket on Trac to
|
It's not always easy for your ticket or your patch to be reviewed quickly. This
|
||||||
ensure that the *Needs tests*, *Needs documentation*, and *Patch needs
|
isn't personal. There are a lot of tickets and pull requests to get through.
|
||||||
improvement* flags are unchecked once you've addressed all review comments.
|
|
||||||
|
|
||||||
Remember that Django has an eight-month release cycle, so there's plenty of
|
Keeping your patch up to date is important. Review the ticket on Trac to ensure
|
||||||
time for your patch to be reviewed.
|
that the *Needs tests*, *Needs documentation*, and *Patch needs improvement*
|
||||||
|
flags are unchecked once you've addressed all review comments.
|
||||||
|
|
||||||
Finally, a well-timed reminder can help. See :ref:`contributing code FAQ
|
Remember that Django has an eight-month release cycle, so there's plenty of
|
||||||
<new-contributors-faq>` for ideas here.
|
time for your patch to be reviewed.
|
||||||
|
|
||||||
|
Finally, a well-timed reminder can help. See :ref:`contributing code FAQ
|
||||||
|
<new-contributors-faq>` for ideas here.
|
||||||
|
|
||||||
.. _easy pickings: https://code.djangoproject.com/query?status=!closed&easy=1
|
.. _easy pickings: https://code.djangoproject.com/query?status=!closed&easy=1
|
||||||
|
@ -49,8 +49,8 @@ attribute easily tells us what and who each ticket is waiting on.
|
|||||||
Since a picture is worth a thousand words, let's start there:
|
Since a picture is worth a thousand words, let's start there:
|
||||||
|
|
||||||
.. image:: /internals/_images/triage_process.*
|
.. image:: /internals/_images/triage_process.*
|
||||||
:height: 501
|
:height: 750
|
||||||
:width: 400
|
:width: 600
|
||||||
:alt: Django's ticket triage workflow
|
:alt: Django's ticket triage workflow
|
||||||
|
|
||||||
We've got two roles in this diagram:
|
We've got two roles in this diagram:
|
||||||
|
@ -417,7 +417,7 @@ Model style
|
|||||||
* All database fields
|
* All database fields
|
||||||
* Custom manager attributes
|
* Custom manager attributes
|
||||||
* ``class Meta``
|
* ``class Meta``
|
||||||
* ``def __str__()``
|
* ``def __str__()`` and other Python magic methods
|
||||||
* ``def save()``
|
* ``def save()``
|
||||||
* ``def get_absolute_url()``
|
* ``def get_absolute_url()``
|
||||||
* Any custom methods
|
* Any custom methods
|
||||||
|
@ -47,10 +47,14 @@ and time availability), claim it by following these steps:
|
|||||||
any activity, it's probably safe to reassign it to yourself.
|
any activity, it's probably safe to reassign it to yourself.
|
||||||
|
|
||||||
* Log into your account, if you haven't already, by clicking "GitHub Login"
|
* Log into your account, if you haven't already, by clicking "GitHub Login"
|
||||||
or "DjangoProject Login" in the upper left of the ticket page.
|
or "DjangoProject Login" in the upper left of the ticket page. Once logged
|
||||||
|
in, you can then click the "Modify Ticket" button near the bottom of the
|
||||||
|
page.
|
||||||
|
|
||||||
* Claim the ticket by clicking the "assign to myself" radio button under
|
* Claim the ticket by clicking the "assign to" radio button in the "Action"
|
||||||
"Action" near the bottom of the page, then click "Submit changes."
|
section. Your username will be filled in the text box by default.
|
||||||
|
|
||||||
|
* Finally click the "Submit changes" button at the bottom to save.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
The Django software foundation requests that anyone contributing more than
|
The Django software foundation requests that anyone contributing more than
|
||||||
@ -114,7 +118,7 @@ requirements:
|
|||||||
feature, the change should also contain documentation.
|
feature, the change should also contain documentation.
|
||||||
|
|
||||||
When you think your work is ready to be reviewed, send :doc:`a GitHub pull
|
When you think your work is ready to be reviewed, send :doc:`a GitHub pull
|
||||||
request <working-with-git>`.
|
request <working-with-git>`.
|
||||||
If you can't send a pull request for some reason, you can also use patches in
|
If you can't send a pull request for some reason, you can also use patches in
|
||||||
Trac. When using this style, follow these guidelines.
|
Trac. When using this style, follow these guidelines.
|
||||||
|
|
||||||
@ -140,20 +144,63 @@ Regardless of the way you submit your work, follow these steps.
|
|||||||
.. _ticket tracker: https://code.djangoproject.com/
|
.. _ticket tracker: https://code.djangoproject.com/
|
||||||
.. _Development dashboard: https://dashboard.djangoproject.com/
|
.. _Development dashboard: https://dashboard.djangoproject.com/
|
||||||
|
|
||||||
Non-trivial contributions
|
Contributions which require community feedback
|
||||||
=========================
|
==============================================
|
||||||
|
|
||||||
A "non-trivial" contribution is one that is more than a small bug fix. It's a
|
A wider community discussion is required when a patch introduces new Django
|
||||||
change that introduces new Django functionality and makes some sort of design
|
functionality and makes some sort of design decision. This is especially
|
||||||
decision.
|
important if the approach involves a :ref:`deprecation <deprecating-a-feature>`
|
||||||
|
or introduces breaking changes.
|
||||||
|
|
||||||
If you provide a non-trivial change, include evidence that alternatives have
|
The following are different approaches for gaining feedback from the community.
|
||||||
been discussed on the `Django Forum`_ or |django-developers| list.
|
|
||||||
|
|
||||||
If you're not sure whether your contribution should be considered non-trivial,
|
The Django Forum or django-developers mailing list
|
||||||
ask on the ticket for opinions.
|
--------------------------------------------------
|
||||||
|
|
||||||
|
You can propose a change on the `Django Forum`_ or |django-developers| mailing
|
||||||
|
list. You should explain the need for the change, go into details of the
|
||||||
|
approach and discuss alternatives.
|
||||||
|
|
||||||
|
Please include a link to such discussions in your contributions.
|
||||||
|
|
||||||
|
Third party package
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
Django does not accept experimental features. All features must follow our
|
||||||
|
:ref:`deprecation policy <internal-release-deprecation-policy>`. Hence, it can
|
||||||
|
take months or years for Django to iterate on an API design.
|
||||||
|
|
||||||
|
If you need user feedback on a public interface, it is better to create a
|
||||||
|
third-party package first. You can iterate on the public API much faster, while
|
||||||
|
also validating the need for the feature.
|
||||||
|
|
||||||
|
Once this package becomes stable and there are clear benefits of incorporating
|
||||||
|
aspects into Django core, starting a discussion on the `Django Forum`_ or
|
||||||
|
|django-developers| mailing list would be the next step.
|
||||||
|
|
||||||
|
Django Enhancement Proposal (DEP)
|
||||||
|
---------------------------------
|
||||||
|
|
||||||
|
Similar to Python’s PEPs, Django has `Django Enhancement Proposals`_ or DEPs. A
|
||||||
|
DEP is a design document which provides information to the Django community, or
|
||||||
|
describes a new feature or process for Django. They provide concise technical
|
||||||
|
specifications of features, along with rationales. DEPs are also the primary
|
||||||
|
mechanism for proposing and collecting community input on major new features.
|
||||||
|
|
||||||
|
Before considering writing a DEP, it is recommended to first open a discussion
|
||||||
|
on the `Django Forum`_ or |django-developers| mailing list. This allows the
|
||||||
|
community to provide feedback and helps refine the proposal. Once the DEP is
|
||||||
|
ready the :ref:`Steering Council <steering-council>` votes on whether to accept
|
||||||
|
it.
|
||||||
|
|
||||||
|
Some examples of DEPs that have been approved and fully implemented:
|
||||||
|
|
||||||
|
* `DEP 181: ORM Expressions <https://github.com/django/deps/blob/main/final/0181-orm-expressions.rst>`_
|
||||||
|
* `DEP 182: Multiple Template Engines <https://github.com/django/deps/blob/main/final/0182-multiple-template-engines.rst>`_
|
||||||
|
* `DEP 201: Simplified routing syntax <https://github.com/django/deps/blob/main/final/0201-simplified-routing-syntax.rst>`_
|
||||||
|
|
||||||
.. _Django Forum: https://forum.djangoproject.com/
|
.. _Django Forum: https://forum.djangoproject.com/
|
||||||
|
.. _Django Enhancement Proposals: https://github.com/django/deps
|
||||||
|
|
||||||
.. _deprecating-a-feature:
|
.. _deprecating-a-feature:
|
||||||
|
|
||||||
|
@ -322,7 +322,6 @@ dependencies:
|
|||||||
* :pypi:`numpy`
|
* :pypi:`numpy`
|
||||||
* :pypi:`Pillow` 6.2.1+
|
* :pypi:`Pillow` 6.2.1+
|
||||||
* :pypi:`PyYAML`
|
* :pypi:`PyYAML`
|
||||||
* :pypi:`pytz` (required)
|
|
||||||
* :pypi:`pywatchman`
|
* :pypi:`pywatchman`
|
||||||
* :pypi:`redis` 3.4+
|
* :pypi:`redis` 3.4+
|
||||||
* :pypi:`setuptools`
|
* :pypi:`setuptools`
|
||||||
|
@ -159,9 +159,14 @@ Spelling check
|
|||||||
|
|
||||||
Before you commit your docs, it's a good idea to run the spelling checker.
|
Before you commit your docs, it's a good idea to run the spelling checker.
|
||||||
You'll need to install :pypi:`sphinxcontrib-spelling` first. Then from the
|
You'll need to install :pypi:`sphinxcontrib-spelling` first. Then from the
|
||||||
``docs`` directory, run ``make spelling``. Wrong words (if any) along with the
|
``docs`` directory, run:
|
||||||
file and line number where they occur will be saved to
|
|
||||||
``_build/spelling/output.txt``.
|
.. console::
|
||||||
|
|
||||||
|
$ make spelling
|
||||||
|
|
||||||
|
Wrong words (if any) along with the file and line number where they occur will
|
||||||
|
be saved to ``_build/spelling/output.txt``.
|
||||||
|
|
||||||
If you encounter false-positives (error output that actually is correct), do
|
If you encounter false-positives (error output that actually is correct), do
|
||||||
one of the following:
|
one of the following:
|
||||||
@ -179,10 +184,21 @@ Link check
|
|||||||
|
|
||||||
Links in documentation can become broken or changed such that they are no
|
Links in documentation can become broken or changed such that they are no
|
||||||
longer the canonical link. Sphinx provides a builder that can check whether the
|
longer the canonical link. Sphinx provides a builder that can check whether the
|
||||||
links in the documentation are working. From the ``docs`` directory, run ``make
|
links in the documentation are working. From the ``docs`` directory, run:
|
||||||
linkcheck``. Output is printed to the terminal, but can also be found in
|
|
||||||
|
.. console::
|
||||||
|
|
||||||
|
$ make linkcheck
|
||||||
|
|
||||||
|
Output is printed to the terminal, but can also be found in
|
||||||
``_build/linkcheck/output.txt`` and ``_build/linkcheck/output.json``.
|
``_build/linkcheck/output.txt`` and ``_build/linkcheck/output.json``.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
The execution of the command requires an internet connection and takes
|
||||||
|
several minutes to complete, because the command tests all the links
|
||||||
|
that are found in the documentation.
|
||||||
|
|
||||||
Entries that have a status of "working" are fine, those that are "unchecked" or
|
Entries that have a status of "working" are fine, those that are "unchecked" or
|
||||||
"ignored" have been skipped because they either cannot be checked or have
|
"ignored" have been skipped because they either cannot be checked or have
|
||||||
matched ignore rules in the configuration.
|
matched ignore rules in the configuration.
|
||||||
@ -290,7 +306,8 @@ documentation:
|
|||||||
display a link with the title "auth".
|
display a link with the title "auth".
|
||||||
|
|
||||||
* All Python code blocks should be formatted using the :pypi:`blacken-docs`
|
* All Python code blocks should be formatted using the :pypi:`blacken-docs`
|
||||||
auto-formatter. This will be run by ``pre-commit`` if that is configured.
|
auto-formatter. This will be run by :ref:`pre-commit
|
||||||
|
<coding-style-pre-commit>` if that is configured.
|
||||||
|
|
||||||
* Use :mod:`~sphinx.ext.intersphinx` to reference Python's and Sphinx'
|
* Use :mod:`~sphinx.ext.intersphinx` to reference Python's and Sphinx'
|
||||||
documentation.
|
documentation.
|
||||||
@ -324,8 +341,9 @@ documentation:
|
|||||||
Five
|
Five
|
||||||
^^^^
|
^^^^
|
||||||
|
|
||||||
* Use :rst:role:`:rfc:<rfc>` to reference RFC and try to link to the relevant
|
* Use :rst:role:`:rfc:<rfc>` to reference a Request for Comments (RFC) and
|
||||||
section if possible. For example, use ``:rfc:`2324#section-2.3.2``` or
|
try to link to the relevant section if possible. For example, use
|
||||||
|
``:rfc:`2324#section-2.3.2``` or
|
||||||
``:rfc:`Custom link text <2324#section-2.3.2>```.
|
``:rfc:`Custom link text <2324#section-2.3.2>```.
|
||||||
|
|
||||||
* Use :rst:role:`:pep:<pep>` to reference a Python Enhancement Proposal (PEP)
|
* Use :rst:role:`:pep:<pep>` to reference a Python Enhancement Proposal (PEP)
|
||||||
@ -339,6 +357,9 @@ documentation:
|
|||||||
also need to define a reference to the documentation for that environment
|
also need to define a reference to the documentation for that environment
|
||||||
variable using :rst:dir:`.. envvar:: <envvar>`.
|
variable using :rst:dir:`.. envvar:: <envvar>`.
|
||||||
|
|
||||||
|
* Use :rst:role:`:cve:<cve>` to reference a Common Vulnerabilities and
|
||||||
|
Exposures (CVE) identifier. For example, use ``:cve:`2019-14232```.
|
||||||
|
|
||||||
Django-specific markup
|
Django-specific markup
|
||||||
======================
|
======================
|
||||||
|
|
||||||
@ -518,7 +539,7 @@ Minimizing images
|
|||||||
Optimize image compression where possible. For PNG files, use OptiPNG and
|
Optimize image compression where possible. For PNG files, use OptiPNG and
|
||||||
AdvanceCOMP's ``advpng``:
|
AdvanceCOMP's ``advpng``:
|
||||||
|
|
||||||
.. code-block:: console
|
.. console::
|
||||||
|
|
||||||
$ cd docs
|
$ cd docs
|
||||||
$ optipng -o7 -zm1-9 -i0 -strip all `find . -type f -not -path "./_build/*" -name "*.png"`
|
$ optipng -o7 -zm1-9 -i0 -strip all `find . -type f -not -path "./_build/*" -name "*.png"`
|
||||||
@ -619,6 +640,10 @@ included in the Django repository and the releases as
|
|||||||
``docs/man/django-admin.1``. There isn't a need to update this file when
|
``docs/man/django-admin.1``. There isn't a need to update this file when
|
||||||
updating the documentation, as it's updated once as part of the release process.
|
updating the documentation, as it's updated once as part of the release process.
|
||||||
|
|
||||||
To generate an updated version of the man page, run ``make man`` in the
|
To generate an updated version of the man page, in the ``docs`` directory, run:
|
||||||
``docs`` directory. The new man page will be written in
|
|
||||||
``docs/_build/man/django-admin.1``.
|
.. console::
|
||||||
|
|
||||||
|
$ make man
|
||||||
|
|
||||||
|
The new man page will be written in ``docs/_build/man/django-admin.1``.
|
||||||
|
@ -18,6 +18,10 @@ details on these changes.
|
|||||||
* The ``all`` keyword argument of ``django.contrib.staticfiles.finders.find()``
|
* The ``all`` keyword argument of ``django.contrib.staticfiles.finders.find()``
|
||||||
will be removed.
|
will be removed.
|
||||||
|
|
||||||
|
* The fallback to ``request.user`` when ``user`` is ``None`` in
|
||||||
|
``django.contrib.auth.login()`` and ``django.contrib.auth.alogin()`` will be
|
||||||
|
removed.
|
||||||
|
|
||||||
.. _deprecation-removed-in-6.0:
|
.. _deprecation-removed-in-6.0:
|
||||||
|
|
||||||
6.0
|
6.0
|
||||||
|
@ -624,9 +624,9 @@ need to be done by the releaser.
|
|||||||
message, add a "refs #XXXX" to the original ticket where the deprecation
|
message, add a "refs #XXXX" to the original ticket where the deprecation
|
||||||
began if possible.
|
began if possible.
|
||||||
|
|
||||||
#. Remove ``.. versionadded::``, ``.. versionadded::``, and ``.. deprecated::``
|
#. Remove ``.. versionadded::``, ``.. versionchanged::``, and
|
||||||
annotations in the documentation from two releases ago. For example, in
|
``.. deprecated::`` annotations in the documentation from two releases ago.
|
||||||
Django 4.2, notes for 4.0 will be removed.
|
For example, in Django 4.2, notes for 4.0 will be removed.
|
||||||
|
|
||||||
#. Add the new branch to `Read the Docs
|
#. Add the new branch to `Read the Docs
|
||||||
<https://readthedocs.org/projects/django/>`_. Since the automatically
|
<https://readthedocs.org/projects/django/>`_. Since the automatically
|
||||||
|
@ -38,6 +38,41 @@ action to be taken, you may receive further followup emails.
|
|||||||
|
|
||||||
.. _our public Trac instance: https://code.djangoproject.com/query
|
.. _our public Trac instance: https://code.djangoproject.com/query
|
||||||
|
|
||||||
|
.. _security-report-evaluation:
|
||||||
|
|
||||||
|
How does Django evaluate a report
|
||||||
|
=================================
|
||||||
|
|
||||||
|
These are criteria used by the security team when evaluating whether a report
|
||||||
|
requires a security release:
|
||||||
|
|
||||||
|
* The vulnerability is within a :ref:`supported version <security-support>` of
|
||||||
|
Django.
|
||||||
|
|
||||||
|
* The vulnerability applies to a production-grade Django application. This means
|
||||||
|
the following do not require a security release:
|
||||||
|
|
||||||
|
* Exploits that only affect local development, for example when using
|
||||||
|
:djadmin:`runserver`.
|
||||||
|
* Exploits which fail to follow security best practices, such as failure to
|
||||||
|
sanitize user input. For other examples, see our :ref:`security
|
||||||
|
documentation <cross-site-scripting>`.
|
||||||
|
* Exploits in AI generated code that do not adhere to security best practices.
|
||||||
|
|
||||||
|
The security team may conclude that the source of the vulnerability is within
|
||||||
|
the Python standard library, in which case the reporter will be asked to report
|
||||||
|
the vulnerability to the Python core team. For further details see the `Python
|
||||||
|
security guidelines <https://www.python.org/dev/security/>`_.
|
||||||
|
|
||||||
|
On occasion, a security release may be issued to help resolve a security
|
||||||
|
vulnerability within a popular third-party package. These reports should come
|
||||||
|
from the package maintainers.
|
||||||
|
|
||||||
|
If you are unsure whether your finding meets these criteria, please still report
|
||||||
|
it :ref:`privately by emailing security@djangoproject.com
|
||||||
|
<reporting-security-issues>`. The security team will review your report and
|
||||||
|
recommend the correct course of action.
|
||||||
|
|
||||||
.. _security-support:
|
.. _security-support:
|
||||||
|
|
||||||
Supported versions
|
Supported versions
|
||||||
|
@ -217,8 +217,7 @@ a dependency for one or more of the Python packages. Consult the failing
|
|||||||
package's documentation or search the web with the error message that you
|
package's documentation or search the web with the error message that you
|
||||||
encounter.
|
encounter.
|
||||||
|
|
||||||
Now we are ready to run the test suite. If you're using GNU/Linux, macOS, or
|
Now we are ready to run the test suite:
|
||||||
some other flavor of Unix, run:
|
|
||||||
|
|
||||||
.. console::
|
.. console::
|
||||||
|
|
||||||
|
@ -309,7 +309,7 @@ Here's what the "base.html" template, including the use of :doc:`static files
|
|||||||
:caption: ``templates/base.html``
|
:caption: ``templates/base.html``
|
||||||
|
|
||||||
{% load static %}
|
{% load static %}
|
||||||
<html>
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>{% block title %}{% endblock %}</title>
|
<title>{% block title %}{% endblock %}</title>
|
||||||
</head>
|
</head>
|
||||||
|
@ -6,7 +6,7 @@ This advanced tutorial begins where :doc:`Tutorial 8 </intro/tutorial08>`
|
|||||||
left off. We'll be turning our web-poll into a standalone Python package
|
left off. We'll be turning our web-poll into a standalone Python package
|
||||||
you can reuse in new projects and share with other people.
|
you can reuse in new projects and share with other people.
|
||||||
|
|
||||||
If you haven't recently completed Tutorials 1–7, we encourage you to review
|
If you haven't recently completed Tutorials 1–8, we encourage you to review
|
||||||
these so that your example project matches the one described below.
|
these so that your example project matches the one described below.
|
||||||
|
|
||||||
Reusability matters
|
Reusability matters
|
||||||
|
@ -22,10 +22,11 @@ Installing Django Debug Toolbar
|
|||||||
===============================
|
===============================
|
||||||
|
|
||||||
Django Debug Toolbar is a useful tool for debugging Django web applications.
|
Django Debug Toolbar is a useful tool for debugging Django web applications.
|
||||||
It's a third-party package maintained by the `Jazzband
|
It's a third-party package that is maintained by the community organization
|
||||||
<https://jazzband.co>`_ organization. The toolbar helps you understand how your
|
`Django Commons <https://github.com/django-commons>`_. The toolbar helps you
|
||||||
application functions and to identify problems. It does so by providing panels
|
understand how your application functions and to identify problems. It does so
|
||||||
that provide debug information about the current request and response.
|
by providing panels that provide debug information about the current request
|
||||||
|
and response.
|
||||||
|
|
||||||
To install a third-party application like the toolbar, you need to install
|
To install a third-party application like the toolbar, you need to install
|
||||||
the package by running the below command within an activated virtual
|
the package by running the below command within an activated virtual
|
||||||
@ -67,7 +68,7 @@ resolve the issue yourself, there are options available to you.
|
|||||||
<https://django-debug-toolbar.readthedocs.io/en/latest/tips.html>`_ that
|
<https://django-debug-toolbar.readthedocs.io/en/latest/tips.html>`_ that
|
||||||
outlines troubleshooting options.
|
outlines troubleshooting options.
|
||||||
#. Search for similar issues on the package's issue tracker. Django Debug
|
#. Search for similar issues on the package's issue tracker. Django Debug
|
||||||
Toolbar’s is `on GitHub <https://github.com/jazzband/django-debug-toolbar/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc>`_.
|
Toolbar’s is `on GitHub <https://github.com/django-commons/django-debug-toolbar/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc>`_.
|
||||||
#. Consult the `Django Forum <https://forum.djangoproject.com/>`_.
|
#. Consult the `Django Forum <https://forum.djangoproject.com/>`_.
|
||||||
#. Join the `Django Discord server <https://discord.gg/xcRH6mN4fa>`_.
|
#. Join the `Django Discord server <https://discord.gg/xcRH6mN4fa>`_.
|
||||||
#. Join the #Django IRC channel on `Libera.chat <https://libera.chat/>`_.
|
#. Join the #Django IRC channel on `Libera.chat <https://libera.chat/>`_.
|
||||||
|
@ -77,6 +77,7 @@ Django's system checks are organized using the following tags:
|
|||||||
* ``async_support``: Checks asynchronous-related configuration.
|
* ``async_support``: Checks asynchronous-related configuration.
|
||||||
* ``caches``: Checks cache related configuration.
|
* ``caches``: Checks cache related configuration.
|
||||||
* ``compatibility``: Flags potential problems with version upgrades.
|
* ``compatibility``: Flags potential problems with version upgrades.
|
||||||
|
* ``commands``: Checks custom management commands related configuration.
|
||||||
* ``database``: Checks database-related configuration issues. Database checks
|
* ``database``: Checks database-related configuration issues. Database checks
|
||||||
are not run by default because they do more than static code analysis as
|
are not run by default because they do more than static code analysis as
|
||||||
regular checks do. They are only run by the :djadmin:`migrate` command or if
|
regular checks do. They are only run by the :djadmin:`migrate` command or if
|
||||||
@ -428,6 +429,14 @@ Models
|
|||||||
* **models.W047**: ``<database>`` does not support unique constraints with
|
* **models.W047**: ``<database>`` does not support unique constraints with
|
||||||
nulls distinct.
|
nulls distinct.
|
||||||
|
|
||||||
|
Management Commands
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
The following checks verify custom management commands are correctly configured:
|
||||||
|
|
||||||
|
* **commands.E001**: The ``migrate`` and ``makemigrations`` commands must have
|
||||||
|
the same ``autodetector``.
|
||||||
|
|
||||||
Security
|
Security
|
||||||
--------
|
--------
|
||||||
|
|
||||||
|
@ -31,6 +31,8 @@ Once those steps are complete, you can start browsing the documentation by
|
|||||||
going to your admin interface and clicking the "Documentation" link in the
|
going to your admin interface and clicking the "Documentation" link in the
|
||||||
upper right of the page.
|
upper right of the page.
|
||||||
|
|
||||||
|
.. _admindocs-helpers:
|
||||||
|
|
||||||
Documentation helpers
|
Documentation helpers
|
||||||
=====================
|
=====================
|
||||||
|
|
||||||
@ -47,13 +49,23 @@ Template filters ``:filter:`filtername```
|
|||||||
Templates ``:template:`path/to/template.html```
|
Templates ``:template:`path/to/template.html```
|
||||||
================= =======================
|
================= =======================
|
||||||
|
|
||||||
|
Each of these support custom link text with the format
|
||||||
|
``:role:`link text <link>```. For example, ``:tag:`block <built_in-block>```.
|
||||||
|
|
||||||
|
.. versionchanged:: 5.2
|
||||||
|
|
||||||
|
Support for custom link text was added.
|
||||||
|
|
||||||
|
.. _admindocs-model-reference:
|
||||||
|
|
||||||
Model reference
|
Model reference
|
||||||
===============
|
===============
|
||||||
|
|
||||||
The **models** section of the ``admindocs`` page describes each model in the
|
The **models** section of the ``admindocs`` page describes each model that the
|
||||||
system along with all the fields, properties, and methods available on it.
|
user has access to along with all the fields, properties, and methods available
|
||||||
Relationships to other models appear as hyperlinks. Descriptions are pulled
|
on it. Relationships to other models appear as hyperlinks. Descriptions are
|
||||||
from ``help_text`` attributes on fields or from docstrings on model methods.
|
pulled from ``help_text`` attributes on fields or from docstrings on model
|
||||||
|
methods.
|
||||||
|
|
||||||
A model with useful documentation might look like this::
|
A model with useful documentation might look like this::
|
||||||
|
|
||||||
@ -77,6 +89,11 @@ A model with useful documentation might look like this::
|
|||||||
"""Makes the blog entry live on the site."""
|
"""Makes the blog entry live on the site."""
|
||||||
...
|
...
|
||||||
|
|
||||||
|
.. versionchanged:: 5.2
|
||||||
|
|
||||||
|
Access was restricted to only allow users with model view or change
|
||||||
|
permissions.
|
||||||
|
|
||||||
View reference
|
View reference
|
||||||
==============
|
==============
|
||||||
|
|
||||||
|
@ -337,7 +337,8 @@ subclass::
|
|||||||
If neither ``fields`` nor :attr:`~ModelAdmin.fieldsets` options are present,
|
If neither ``fields`` nor :attr:`~ModelAdmin.fieldsets` options are present,
|
||||||
Django will default to displaying each field that isn't an ``AutoField`` and
|
Django will default to displaying each field that isn't an ``AutoField`` and
|
||||||
has ``editable=True``, in a single fieldset, in the same order as the fields
|
has ``editable=True``, in a single fieldset, in the same order as the fields
|
||||||
are defined in the model.
|
are defined in the model, followed by any fields defined in
|
||||||
|
:attr:`~ModelAdmin.readonly_fields`.
|
||||||
|
|
||||||
.. attribute:: ModelAdmin.fieldsets
|
.. attribute:: ModelAdmin.fieldsets
|
||||||
|
|
||||||
@ -1465,6 +1466,27 @@ templates used by the :class:`ModelAdmin` views:
|
|||||||
|
|
||||||
See also :ref:`saving-objects-in-the-formset`.
|
See also :ref:`saving-objects-in-the-formset`.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
All hooks that return a ``ModelAdmin`` property return the property itself
|
||||||
|
rather than a copy of its value. Dynamically modifying the value can lead
|
||||||
|
to surprising results.
|
||||||
|
|
||||||
|
Let's take :meth:`ModelAdmin.get_readonly_fields` as an example::
|
||||||
|
|
||||||
|
class PersonAdmin(admin.ModelAdmin):
|
||||||
|
readonly_fields = ["name"]
|
||||||
|
|
||||||
|
def get_readonly_fields(self, request, obj=None):
|
||||||
|
readonly = super().get_readonly_fields(request, obj)
|
||||||
|
if not request.user.is_superuser:
|
||||||
|
readonly.append("age") # Edits the class attribute.
|
||||||
|
return readonly
|
||||||
|
|
||||||
|
This results in ``readonly_fields`` becoming
|
||||||
|
``["name", "age", "age", ...]``, even for a superuser, as ``"age"`` is added
|
||||||
|
each time non-superuser visits the page.
|
||||||
|
|
||||||
.. method:: ModelAdmin.get_ordering(request)
|
.. method:: ModelAdmin.get_ordering(request)
|
||||||
|
|
||||||
The ``get_ordering`` method takes a ``request`` as parameter and
|
The ``get_ordering`` method takes a ``request`` as parameter and
|
||||||
@ -2229,6 +2251,7 @@ information.
|
|||||||
inlines to a model by specifying them in a ``ModelAdmin.inlines``::
|
inlines to a model by specifying them in a ``ModelAdmin.inlines``::
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from myapp.models import Author, Book
|
||||||
|
|
||||||
|
|
||||||
class BookInline(admin.TabularInline):
|
class BookInline(admin.TabularInline):
|
||||||
@ -2240,6 +2263,9 @@ information.
|
|||||||
BookInline,
|
BookInline,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.register(Author, AuthorAdmin)
|
||||||
|
|
||||||
Django provides two subclasses of ``InlineModelAdmin`` and they are:
|
Django provides two subclasses of ``InlineModelAdmin`` and they are:
|
||||||
|
|
||||||
* :class:`~django.contrib.admin.TabularInline`
|
* :class:`~django.contrib.admin.TabularInline`
|
||||||
@ -2472,6 +2498,10 @@ Take this model for instance::
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class Person(models.Model):
|
||||||
|
name = models.CharField(max_length=128)
|
||||||
|
|
||||||
|
|
||||||
class Friendship(models.Model):
|
class Friendship(models.Model):
|
||||||
to_person = models.ForeignKey(
|
to_person = models.ForeignKey(
|
||||||
Person, on_delete=models.CASCADE, related_name="friends"
|
Person, on_delete=models.CASCADE, related_name="friends"
|
||||||
@ -2485,7 +2515,7 @@ you need to explicitly define the foreign key since it is unable to do so
|
|||||||
automatically::
|
automatically::
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from myapp.models import Friendship
|
from myapp.models import Friendship, Person
|
||||||
|
|
||||||
|
|
||||||
class FriendshipInline(admin.TabularInline):
|
class FriendshipInline(admin.TabularInline):
|
||||||
@ -2498,6 +2528,9 @@ automatically::
|
|||||||
FriendshipInline,
|
FriendshipInline,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.register(Person, PersonAdmin)
|
||||||
|
|
||||||
Working with many-to-many models
|
Working with many-to-many models
|
||||||
--------------------------------
|
--------------------------------
|
||||||
|
|
||||||
@ -2526,24 +2559,22 @@ If you want to display many-to-many relations using an inline, you can do
|
|||||||
so by defining an ``InlineModelAdmin`` object for the relationship::
|
so by defining an ``InlineModelAdmin`` object for the relationship::
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from myapp.models import Group
|
||||||
|
|
||||||
|
|
||||||
class MembershipInline(admin.TabularInline):
|
class MembershipInline(admin.TabularInline):
|
||||||
model = Group.members.through
|
model = Group.members.through
|
||||||
|
|
||||||
|
|
||||||
class PersonAdmin(admin.ModelAdmin):
|
|
||||||
inlines = [
|
|
||||||
MembershipInline,
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class GroupAdmin(admin.ModelAdmin):
|
class GroupAdmin(admin.ModelAdmin):
|
||||||
inlines = [
|
inlines = [
|
||||||
MembershipInline,
|
MembershipInline,
|
||||||
]
|
]
|
||||||
exclude = ["members"]
|
exclude = ["members"]
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.register(Group, GroupAdmin)
|
||||||
|
|
||||||
There are two features worth noting in this example.
|
There are two features worth noting in this example.
|
||||||
|
|
||||||
Firstly - the ``MembershipInline`` class references ``Group.members.through``.
|
Firstly - the ``MembershipInline`` class references ``Group.members.through``.
|
||||||
|
@ -30,10 +30,7 @@ Fields
|
|||||||
|
|
||||||
The ``max_length`` should be sufficient for many use cases. If you need
|
The ``max_length`` should be sufficient for many use cases. If you need
|
||||||
a longer length, please use a :ref:`custom user model
|
a longer length, please use a :ref:`custom user model
|
||||||
<specifying-custom-user-model>`. If you use MySQL with the ``utf8mb4``
|
<specifying-custom-user-model>`.
|
||||||
encoding (recommended for proper Unicode support), specify at most
|
|
||||||
``max_length=191`` because MySQL can only create unique indexes with
|
|
||||||
191 characters in that case by default.
|
|
||||||
|
|
||||||
.. attribute:: first_name
|
.. attribute:: first_name
|
||||||
|
|
||||||
@ -54,7 +51,8 @@ Fields
|
|||||||
|
|
||||||
Required. A hash of, and metadata about, the password. (Django doesn't
|
Required. A hash of, and metadata about, the password. (Django doesn't
|
||||||
store the raw password.) Raw passwords can be arbitrarily long and can
|
store the raw password.) Raw passwords can be arbitrarily long and can
|
||||||
contain any character. See the :doc:`password documentation
|
contain any character. The metadata in this field may mark the password
|
||||||
|
as unusable. See the :doc:`password documentation
|
||||||
</topics/auth/passwords>`.
|
</topics/auth/passwords>`.
|
||||||
|
|
||||||
.. attribute:: groups
|
.. attribute:: groups
|
||||||
@ -175,8 +173,9 @@ Methods
|
|||||||
|
|
||||||
.. method:: set_unusable_password()
|
.. method:: set_unusable_password()
|
||||||
|
|
||||||
Marks the user as having no password set. This isn't the same as
|
Marks the user as having no password set by updating the metadata in
|
||||||
having a blank string for a password.
|
the :attr:`~django.contrib.auth.models.User.password` field. This isn't
|
||||||
|
the same as having a blank string for a password.
|
||||||
:meth:`~django.contrib.auth.models.User.check_password()` for this user
|
:meth:`~django.contrib.auth.models.User.check_password()` for this user
|
||||||
will never return ``True``. Doesn't save the
|
will never return ``True``. Doesn't save the
|
||||||
:class:`~django.contrib.auth.models.User` object.
|
:class:`~django.contrib.auth.models.User` object.
|
||||||
@ -420,8 +419,8 @@ fields:
|
|||||||
|
|
||||||
.. attribute:: content_type
|
.. attribute:: content_type
|
||||||
|
|
||||||
Required. A reference to the ``django_content_type`` database table,
|
Required. A foreign key to the
|
||||||
which contains a record for each installed model.
|
:class:`~django.contrib.contenttypes.models.ContentType` model.
|
||||||
|
|
||||||
.. attribute:: codename
|
.. attribute:: codename
|
||||||
|
|
||||||
@ -680,7 +679,7 @@ The following backends are available in :mod:`django.contrib.auth.backends`:
|
|||||||
user permissions and group permissions. Returns an empty set if
|
user permissions and group permissions. Returns an empty set if
|
||||||
:attr:`~django.contrib.auth.models.AbstractBaseUser.is_anonymous` or
|
:attr:`~django.contrib.auth.models.AbstractBaseUser.is_anonymous` or
|
||||||
:attr:`~django.contrib.auth.models.CustomUser.is_active` is ``False``.
|
:attr:`~django.contrib.auth.models.CustomUser.is_active` is ``False``.
|
||||||
|
|
||||||
.. versionchanged:: 5.2
|
.. versionchanged:: 5.2
|
||||||
|
|
||||||
``aget_all_permissions()`` function was added.
|
``aget_all_permissions()`` function was added.
|
||||||
|
@ -256,7 +256,7 @@ Here's a sample :file:`flatpages/default.html` template:
|
|||||||
.. code-block:: html+django
|
.. code-block:: html+django
|
||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>{{ flatpage.title }}</title>
|
<title>{{ flatpage.title }}</title>
|
||||||
</head>
|
</head>
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user