1
0
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:
Jonathan 2024-11-28 11:31:58 -08:00 committed by GitHub
commit ba6a74d058
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
220 changed files with 4907 additions and 1173 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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, youll be able to edit more user options." %}</p> <p>{% translate "After youve created a user, youll be able to edit more user options." %}</p>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block extrahead %} {% block extrahead %}

View File

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

View File

@ -15,7 +15,7 @@
<h1>{{ name }}</h1> <h1>{{ name }}</h1>
<h2 class="subhead">{{ summary|striptags }}</h2> <h2 class="subhead">{{ summary }}</h2>
{{ body }} {{ body }}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 cant be too similar to your other personal information." "Your password cant 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 cant be a commonly used password.") return _("Your password cant 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 cant be entirely numeric.") return _("Your password cant be entirely numeric.")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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 []

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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&apos;t a bug, doesn&apos;t contain </tspan> <tspan font-family="Helvetica" font-size="12" fill="#595959" x=".8017578" y="25">already rejected, isn&apos;t a bug, doesn&apos;t contain </tspan>
<tspan font-family="Helvetica" font-size="12" fill="#797979" x="1.2792969" y="39">enough information, or can&apos;t be reproduced.</tspan> <tspan font-family="Helvetica" font-size="12" fill="#595959" x="1.2792969" y="39">enough information, or can&apos;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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 17, we encourage you to review If you haven't recently completed Tutorials 18, 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

View File

@ -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
Toolbars is `on GitHub <https://github.com/jazzband/django-debug-toolbar/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc>`_. Toolbars 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/>`_.

View File

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

View File

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

View File

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

View File

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

View File

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