1
0
mirror of https://github.com/django/django.git synced 2025-04-04 21:46:40 +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:
docs:
# OS must be the same as on djangoproject.com.
runs-on: ubuntu-20.04
runs-on: ubuntu-24.04
name: docs
steps:
- name: Checkout

View File

@ -49,4 +49,4 @@ jobs:
run: python -m pip install --upgrade pip setuptools wheel
- run: python -m pip install -r tests/requirements/py3.txt -e .
- 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.12'
- '3.13'
- '3.14-dev'
name: Windows, SQLite, Python ${{ matrix.python-version }}
continue-on-error: true
steps:
@ -35,7 +36,7 @@ jobs:
run: python -m pip install --upgrade pip setuptools wheel
- run: python -m pip install -r tests/requirements/py3.txt -e .
- name: Run tests
run: python tests/runtests.py -v2
run: python -Wall tests/runtests.py -v2
pyc-only:
runs-on: ubuntu-latest
@ -61,7 +62,7 @@ jobs:
find $DJANGO_PACKAGE_ROOT -name '*.py' -print -delete
- run: python -m pip install -r tests/requirements/py3.txt
- name: Run tests
run: python tests/runtests.py --verbosity=2
run: python -Wall tests/runtests.py --verbosity=2
pypy-sqlite:
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 -r tests/requirements/py3.txt -e .
- name: Run tests
run: python tests/runtests.py -v2
run: python -Wall tests/runtests.py -v2
javascript-tests:
runs-on: ubuntu-latest

View File

@ -4,12 +4,13 @@
version: 2
build:
os: ubuntu-20.04
os: ubuntu-24.04
tools:
python: "3.8"
python: "3.12"
sphinx:
configuration: docs/conf.py
fail_on_warning: true
python:
install:

View File

@ -282,6 +282,7 @@ answer newbie questions, and generally made Django that much better:
David Sanders <dsanders11@ucsbalum.com>
David Schein
David Tulig <david.tulig@gmail.com>
David Winiecki <david.winiecki@gmail.com>
David Winterbottom <david.winterbottom@gmail.com>
David Wobrock <david.wobrock@gmail.com>
Davide Ceretti <dav.ceretti@gmail.com>

View File

@ -121,7 +121,7 @@ class Fieldset:
@cached_property
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 "collapse" in self.classes

View File

@ -41,6 +41,7 @@ from django.core.exceptions import (
from django.core.paginator import Paginator
from django.db import models, router, transaction
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.models import (
BaseInlineFormSet,
@ -1177,17 +1178,17 @@ class ModelAdmin(BaseModelAdmin):
# Apply keyword searches.
def construct_search(field_name):
if field_name.startswith("^"):
return "%s__istartswith" % field_name.removeprefix("^")
return "%s__istartswith" % field_name.removeprefix("^"), None
elif field_name.startswith("="):
return "%s__iexact" % field_name.removeprefix("=")
return "%s__iexact" % field_name.removeprefix("="), None
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.
opts = queryset.model._meta
lookup_fields = field_name.split(LOOKUP_SEP)
# Go through the fields, following all relations.
prev_field = None
for path_part in lookup_fields:
for i, path_part in enumerate(lookup_fields):
if path_part == "pk":
path_part = opts.pk.name
try:
@ -1195,21 +1196,40 @@ class ModelAdmin(BaseModelAdmin):
except FieldDoesNotExist:
# Use valid query lookups.
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:
prev_field = field
if hasattr(field, "path_infos"):
# Update opts to follow the relation.
opts = field.path_infos[-1].to_opts
# Otherwise, use the field with icontains.
return "%s__icontains" % field_name
return "%s__icontains" % field_name, None
may_have_duplicates = False
search_fields = self.get_search_fields(request)
if search_fields and search_term:
orm_lookups = [
construct_search(str(search_field)) for search_field in search_fields
]
str_aliases = {}
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 = []
for bit in smart_split(search_term):
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("jsi18n/", wrap(self.i18n_javascript, cacheable=True), name="jsi18n"),
path(
"r/<int:content_type_id>/<path:object_id>/",
"r/<path:content_type_id>/<path:object_id>/",
wrap(contenttype_views.shortcut),
name="view_on_site",
),

View File

@ -299,7 +299,7 @@ input[type="submit"], button {
background-position: 0 -80px;
}
a.selector-chooseall, a.selector-clearall {
.selector-chooseall, .selector-clearall {
align-self: center;
}
@ -649,6 +649,7 @@ input[type="submit"], button {
.related-widget-wrapper .selector {
order: 1;
flex: 1 0 auto;
}
.related-widget-wrapper > a {

View File

@ -235,19 +235,19 @@ fieldset .fieldBox {
background-position: 0 -112px;
}
a.selector-chooseall {
.selector-chooseall {
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;
}
a.selector-clearall {
.selector-clearall {
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;
}

View File

@ -2,7 +2,7 @@
.selector {
display: flex;
flex-grow: 1;
flex: 1;
gap: 0 10px;
}
@ -14,17 +14,20 @@
}
.selector-available, .selector-chosen {
text-align: center;
display: flex;
flex-direction: column;
flex: 1 1;
}
.selector-available h2, .selector-chosen h2 {
.selector-available-title, .selector-chosen-title {
border: 1px solid var(--border-color);
border-radius: 4px 4px 0 0;
}
.selector .helptext {
font-size: 0.6875rem;
}
.selector-chosen .list-footer-display {
border: 1px solid var(--border-color);
border-top: none;
@ -40,14 +43,20 @@
color: var(--breadcrumbs-fg);
}
.selector-chosen h2 {
.selector-chosen-title {
background: var(--secondary);
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);
color: var(--body-quiet-color);
padding: 8px;
}
.selector .selector-filter {
@ -121,6 +130,7 @@
overflow: hidden;
cursor: default;
opacity: 0.55;
border: none;
}
.active.selector-add, .active.selector-remove {
@ -147,7 +157,7 @@
background-position: 0 -80px;
}
a.selector-chooseall, a.selector-clearall {
.selector-chooseall, .selector-clearall {
display: inline-block;
height: 16px;
text-align: left;
@ -158,38 +168,39 @@ a.selector-chooseall, a.selector-clearall {
color: var(--body-quiet-color);
text-decoration: none;
opacity: 0.55;
border: none;
}
a.active.selector-chooseall:focus, a.active.selector-clearall:focus,
a.active.selector-chooseall:hover, a.active.selector-clearall:hover {
.active.selector-chooseall:focus, .active.selector-clearall:focus,
.active.selector-chooseall:hover, .active.selector-clearall:hover {
color: var(--link-fg);
}
a.active.selector-chooseall, a.active.selector-clearall {
.active.selector-chooseall, .active.selector-clearall {
opacity: 1;
}
a.active.selector-chooseall:hover, a.active.selector-clearall:hover {
.active.selector-chooseall:hover, .active.selector-clearall:hover {
cursor: pointer;
}
a.selector-chooseall {
.selector-chooseall {
padding: 0 18px 0 0;
background: url(../img/selector-icons.svg) right -160px no-repeat;
cursor: default;
}
a.active.selector-chooseall:focus, a.active.selector-chooseall:hover {
.active.selector-chooseall:focus, .active.selector-chooseall:hover {
background-position: 100% -176px;
}
a.selector-clearall {
.selector-clearall {
padding: 0 0 0 18px;
background: url(../img/selector-icons.svg) 0 -128px no-repeat;
cursor: default;
}
a.active.selector-clearall:focus, a.active.selector-clearall:hover {
.active.selector-clearall:focus, .active.selector-clearall:hover {
background-position: 0 -144px;
}

View File

@ -15,6 +15,7 @@ Requires core.js and SelectBox.js.
const from_box = document.getElementById(field_id);
from_box.id += '_from'; // change its ID
from_box.className = 'filtered';
from_box.setAttribute('aria-labelledby', field_id + '_from_title');
for (const p of from_box.parentNode.getElementsByTagName('p')) {
if (p.classList.contains("info")) {
@ -38,18 +39,15 @@ Requires core.js and SelectBox.js.
// <div class="selector-available">
const selector_available = quickElement('div', selector_div);
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(
'span', title_available, '',
'class', 'help help-tooltip help-icon',
'title', interpolate(
gettext(
'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]
)
'p',
selector_available_title,
interpolate(gettext('Choose %s by selecting them and then select the "Choose" arrow button.'), [field_name]),
'class', 'helptext'
);
const filter_p = quickElement('p', selector_available, '', 'id', field_id + '_filter');
@ -60,7 +58,7 @@ Requires core.js and SelectBox.js.
quickElement(
'span', search_filter_label, '',
'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(' '));
@ -69,32 +67,44 @@ Requires core.js and SelectBox.js.
filter_input.id = field_id + '_input';
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');
choose_all.className = 'selector-chooseall';
const choose_all = quickElement(
'button',
selector_available,
interpolate(gettext('Choose all %s'), [field_name]),
'id', field_id + '_add_all',
'class', 'selector-chooseall'
);
// <ul class="selector-chooser">
const selector_chooser = quickElement('ul', selector_div);
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');
add_link.className = 'selector-add';
const remove_link = quickElement('a', quickElement('li', selector_chooser), gettext('Remove'), 'title', gettext('Remove'), 'href', '#', 'id', field_id + '_remove_link');
remove_link.className = 'selector-remove';
const add_button = quickElement(
'button',
quickElement('li', selector_chooser),
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">
const selector_chosen = quickElement('div', selector_div, '', 'id', field_id + '_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(
'span', title_chosen, '',
'class', 'help help-tooltip help-icon',
'title', interpolate(
gettext(
'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]
)
'p',
selector_chosen_title,
interpolate(gettext('Remove %s by selecting them and then select the "Remove" arrow button.'), [field_name]),
'class', 'helptext'
);
const filter_selected_p = quickElement('p', selector_chosen, '', 'id', field_id + '_filter_selected');
@ -105,7 +115,7 @@ Requires core.js and SelectBox.js.
quickElement(
'span', search_filter_selected_label, '',
'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(' '));
@ -113,15 +123,27 @@ Requires core.js and SelectBox.js.
const filter_selected_input = quickElement('input', filter_selected_p, '', 'type', 'text', 'placeholder', gettext("Filter"));
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);
to_box.className = 'filtered';
quickElement(
'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');
quickElement('span', warning_footer, '', 'id', field_id + '_list-footer-display-text');
quickElement('span', warning_footer, ' ' + gettext('(click to clear)'), 'class', 'list-footer-display__clear');
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');
clear_all.className = 'selector-clearall';
const clear_all = quickElement(
'button',
selector_chosen,
interpolate(gettext('Remove all %s'), [field_name]),
'id', field_id + '_remove_all',
'class', 'selector-clearall'
);
from_box.name = from_box.name + '_old';
@ -138,10 +160,10 @@ Requires core.js and SelectBox.js.
choose_all.addEventListener('click', function(e) {
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');
});
remove_link.addEventListener('click', function(e) {
remove_button.addEventListener('click', function(e) {
move_selection(e, this, SelectBox.move, field_id + '_to', field_id + '_from');
});
clear_all.addEventListener('click', function(e) {
@ -227,11 +249,11 @@ Requires core.js and SelectBox.js.
const from = document.getElementById(field_id + '_from');
const to = document.getElementById(field_id + '_to');
// 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 + '_remove_link').classList.toggle('active', SelectFilter.any_selected(to));
document.getElementById(field_id + '_add').classList.toggle('active', SelectFilter.any_selected(from));
document.getElementById(field_id + '_remove').classList.toggle('active', SelectFilter.any_selected(to));
// 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 + '_remove_all_link').classList.toggle('active', to.querySelector('option'));
document.getElementById(field_id + '_add_all').classList.toggle('active', from.querySelector('option'));
document.getElementById(field_id + '_remove_all').classList.toggle('active', to.querySelector('option'));
SelectFilter.refresh_filtered_warning(field_id);
},
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
// "add" button in a new table row:
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");
} else {
// 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");
}
}
@ -104,15 +104,15 @@
if (row.is("tr")) {
// If the forms are laid out in table rows, insert
// 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")) {
// If they're laid out as an ordered/unordered list,
// 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 {
// Otherwise, just insert the remove button as the
// 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.
row.find("a." + options.deleteCssClass).on('click', inlineDeleteHandler.bind(this));

View File

@ -13,9 +13,9 @@
{% if cl.result_count != cl.result_list|length %}
<span class="all hidden">{{ selection_note_all }}</span>
<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 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 %}
{% endblock %}

View File

@ -3,7 +3,7 @@
{% block form_top %}
{% 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 %}
{% endblock %}
{% block extrahead %}

View File

@ -1,8 +1,7 @@
{% with name=fieldset.name|default:""|slugify %}
<fieldset class="module aligned {{ fieldset.classes }}"{% if name %} aria-labelledby="{{ prefix }}-{{ id_prefix}}-{{ name }}-{{ id_suffix }}-heading"{% endif %}>
{% if name %}
<fieldset class="module aligned {{ fieldset.classes }}"{% if fieldset.name %} aria-labelledby="{{ prefix }}-{{ id_prefix}}-{{ id_suffix }}-heading"{% endif %}>
{% if fieldset.name %}
{% 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 %}
{% endif %}
{% if fieldset.description %}
@ -36,6 +35,5 @@
{% if not line.fields|length == 1 %}</div>{% endif %}
</div>
{% endfor %}
{% if name and fieldset.is_collapsible %}</details>{% endif %}
{% if fieldset.name and fieldset.is_collapsible %}</details>{% endif %}
</fieldset>
{% endwith %}

View File

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

View File

@ -99,6 +99,21 @@ ROLES = {
"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):
# 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):
if options is None:
options = {}
_, title, target = split_explicit_title(text)
node = docutils.nodes.reference(
rawtext,
text,
title,
refuri=(
urlbase
% (
inliner.document.settings.link_base,
text if is_case_sensitive else text.lower(),
target if is_case_sensitive else target.lower(),
)
),
**options,
@ -242,3 +258,7 @@ def remove_non_capturing_groups(pattern):
final_pattern += pattern[prev_end:start]
prev_end = 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_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.http import Http404
from django.template.engine import Engine
@ -30,7 +35,7 @@ from django.utils.inspect import (
from django.utils.translation import gettext as _
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
MODEL_METHODS_EXCLUDE = ("_", "add_", "delete", "save", "set_")
@ -195,18 +200,31 @@ class ViewDetailView(BaseAdminDocsView):
**{
**kwargs,
"name": view,
"summary": title,
"summary": strip_p_tags(title),
"body": body,
"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):
template_name = "admin_doc/model_index.html"
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})
@ -228,6 +246,8 @@ class ModelDetailView(BaseAdminDocsView):
)
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 = title and utils.parse_rst(title, "model", _("model:") + model_name)
@ -384,7 +404,7 @@ class ModelDetailView(BaseAdminDocsView):
**{
**kwargs,
"name": opts.label,
"summary": title,
"summary": strip_p_tags(title),
"description": body,
"fields": fields,
"methods": methods,

View File

@ -1,11 +1,13 @@
import inspect
import re
import warnings
from django.apps import apps as django_apps
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured, PermissionDenied
from django.middleware.csrf import rotate_token
from django.utils.crypto import constant_time_compare
from django.utils.deprecation import RemovedInDjango61Warning
from django.utils.module_loading import import_string
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
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 = ""
# RemovedInDjango61Warning.
if user is None:
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"):
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):
"""See login()."""
# RemovedInDjango61Warning: When the deprecation ends, replace with:
# session_auth_hash = user.get_session_auth_hash()
session_auth_hash = ""
# RemovedInDjango61Warning.
if user is None:
warnings.warn(
"Fallback to request.user when user is None will be removed.",
RemovedInDjango61Warning,
stacklevel=2,
)
user = await request.auser()
# RemovedInDjango61Warning.
if hasattr(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 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.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)
if asyncio.iscoroutinefunction(view_func):
if iscoroutinefunction(view_func):
async def _view_wrapper(request, *args, **kwargs):
auser = await request.auser()
if asyncio.iscoroutinefunction(test_func):
if iscoroutinefunction(test_func):
test_pass = await test_func(auser)
else:
test_pass = await sync_to_async(test_func)(auser)
@ -51,7 +50,7 @@ def user_passes_test(
else:
def _view_wrapper(request, *args, **kwargs):
if asyncio.iscoroutinefunction(test_func):
if iscoroutinefunction(test_func):
test_pass = async_to_sync(test_func)(request.user)
else:
test_pass = test_func(request.user)
@ -107,7 +106,7 @@ def permission_required(perm, login_url=None, raise_exception=False):
perms = perm
def decorator(view_func):
if asyncio.iscoroutinefunction(view_func):
if iscoroutinefunction(view_func):
async def check_perms(user):
# 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.translation import gettext
from django.utils.translation import gettext_lazy as _
from django.views.decorators.debug import sensitive_variables
UserModel = get_user_model()
logger = logging.getLogger("django.contrib.auth")
@ -122,6 +123,7 @@ class SetPasswordMixin:
)
return password1, password2
@sensitive_variables("password1", "password2")
def validate_passwords(
self,
password1_field_name="password1",
@ -151,6 +153,7 @@ class SetPasswordMixin:
)
self.add_error(password2_field_name, error)
@sensitive_variables("password")
def validate_password_for_user(self, user, password_field_name="password2"):
password = self.cleaned_data.get(password_field_name)
if password:
@ -348,6 +351,7 @@ class AuthenticationForm(forms.Form):
if self.fields["username"].label is None:
self.fields["username"].label = capfirst(self.username_field.verbose_name)
@sensitive_variables()
def clean(self):
username = self.cleaned_data.get("username")
password = self.cleaned_data.get("password")
@ -539,6 +543,7 @@ class PasswordChangeForm(SetPasswordForm):
field_order = ["old_password", "new_password1", "new_password2"]
@sensitive_variables("old_password")
def clean_old_password(self):
"""
Validate that the old_password field is correct.

View File

@ -115,10 +115,12 @@ def get_system_username():
"""
try:
result = getpass.getuser()
except (ImportError, KeyError):
# KeyError will be raised by os.getpwuid() (called by getuser())
# if there is no corresponding entry in the /etc/passwd file
# (a very restricted chroot environment, for example).
except (ImportError, KeyError, OSError):
# TODO: Drop ImportError and KeyError when dropping support for PY312.
# KeyError (Python <3.13) or OSError (Python 3.13+) will be raised by
# 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 result

View File

@ -174,11 +174,15 @@ class UserManager(BaseUserManager):
extra_fields.setdefault("is_superuser", False)
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):
extra_fields.setdefault("is_staff", False)
extra_fields.setdefault("is_superuser", False)
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):
extra_fields.setdefault("is_staff", True)
extra_fields.setdefault("is_superuser", True)
@ -190,6 +194,8 @@ class UserManager(BaseUserManager):
return self._create_user(username, email, password, **extra_fields)
create_superuser.alters_data = True
async def acreate_superuser(
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)
acreate_superuser.alters_data = True
def with_perm(
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):
if len(password) < self.min_length:
raise ValidationError(
ngettext(
"This password is too short. It must contain at least "
"%(min_length)d character.",
"This password is too short. It must contain at least "
"%(min_length)d characters.",
self.min_length,
),
code="password_too_short",
params={"min_length": self.min_length},
)
raise ValidationError(self.get_error_message(), code="password_too_short")
def get_error_message(self):
return ngettext(
"This password is too short. It must contain at least %d character."
% self.min_length,
"This password is too short. It must contain at least %d characters."
% self.min_length,
self.min_length,
)
def get_help_text(self):
return ngettext(
@ -203,11 +202,14 @@ class UserAttributeSimilarityValidator:
except FieldDoesNotExist:
verbose_name = attribute_name
raise ValidationError(
_("The password is too similar to the %(verbose_name)s."),
self.get_error_message(),
code="password_too_similar",
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):
return _(
"Your password cant be too similar to your other personal information."
@ -242,10 +244,13 @@ class CommonPasswordValidator:
def validate(self, password, user=None):
if password.lower().strip() in self.passwords:
raise ValidationError(
_("This password is too common."),
self.get_error_message(),
code="password_too_common",
)
def get_error_message(self):
return _("This password is too common.")
def get_help_text(self):
return _("Your password cant be a commonly used password.")
@ -258,9 +263,12 @@ class NumericPasswordValidator:
def validate(self, password, user=None):
if password.isdigit():
raise ValidationError(
_("This password is entirely numeric."),
self.get_error_message(),
code="password_entirely_numeric",
)
def get_error_message(self):
return _("This password is entirely numeric.")
def get_help_text(self):
return _("Your password cant be entirely numeric.")

View File

@ -45,6 +45,7 @@ class MySQLOperations(BaseSpatialOperations, DatabaseOperations):
"bboverlaps": SpatialOperator(func="MBROverlaps"), # ...
"contained": SpatialOperator(func="MBRWithin"), # ...
"contains": SpatialOperator(func="ST_Contains"),
"coveredby": SpatialOperator(func="MBRCoveredBy"),
"crosses": SpatialOperator(func="ST_Crosses"),
"disjoint": SpatialOperator(func="ST_Disjoint"),
"equals": SpatialOperator(func="ST_Equals"),
@ -57,6 +58,10 @@ class MySQLOperations(BaseSpatialOperations, DatabaseOperations):
}
if self.connection.mysql_is_mariadb:
operators["relate"] = SpatialOperator(func="ST_Relate")
if self.connection.mysql_version < (11, 7):
del operators["coveredby"]
else:
operators["covers"] = SpatialOperator(func="MBRCovers")
return operators
@cached_property
@ -68,7 +73,10 @@ class MySQLOperations(BaseSpatialOperations, DatabaseOperations):
models.Union,
]
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)
return tuple(disallowed_aggregates)
@ -102,7 +110,8 @@ class MySQLOperations(BaseSpatialOperations, DatabaseOperations):
}
if self.connection.mysql_is_mariadb:
unsupported.remove("PointOnSurface")
unsupported.update({"GeoHash", "IsValid"})
if self.connection.mysql_version < (11, 7):
unsupported.update({"GeoHash", "IsValid"})
return unsupported
def geo_db_type(self, f):

View File

@ -64,6 +64,7 @@ class OGRGeometry(GDALBase):
"""Encapsulate an OGR geometry."""
destructor = capi.destroy_geom
geos_support = True
def __init__(self, geom_input, srs=None):
"""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}'."
)
@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 ####
# The SRS property
@ -360,9 +374,14 @@ class OGRGeometry(GDALBase):
@property
def geos(self):
"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
def gml(self):
@ -727,6 +746,18 @@ class Polygon(OGRGeometry):
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.
class GeometryCollection(OGRGeometry):
"The Geometry Collection class."
@ -788,6 +819,14 @@ class MultiPolygon(GeometryCollection):
pass
class MultiSurface(GeometryCollection):
geos_support = False
class MultiCurve(GeometryCollection):
geos_support = False
# Class mapping dictionary (using the OGRwkbGeometryType as the key)
GEO_CLASSES = {
1: Point,
@ -797,7 +836,17 @@ GEO_CLASSES = {
5: MultiLineString,
6: MultiPolygon,
7: GeometryCollection,
8: CircularString,
9: CompoundCurve,
10: CurvePolygon,
11: MultiCurve,
12: MultiSurface,
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
2002: LineString, # LINESTRING M
2003: Polygon, # POLYGON M
@ -805,6 +854,11 @@ GEO_CLASSES = {
2005: MultiLineString, # MULTILINESTRING M
2006: MultiPolygon, # MULTIPOLYGON 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
3002: LineString, # LINESTRING ZM
3003: Polygon, # POLYGON ZM
@ -812,6 +866,11 @@ GEO_CLASSES = {
3005: MultiLineString, # MULTILINESTRING ZM
3006: MultiPolygon, # MULTIPOLYGON 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
2 + OGRGeomType.wkb25bit: LineString, # LINESTRING Z
3 + OGRGeomType.wkb25bit: Polygon, # POLYGON Z

View File

@ -22,6 +22,7 @@ if lib_path:
elif os.name == "nt":
# Windows NT shared libraries
lib_names = [
"gdal309",
"gdal308",
"gdal307",
"gdal306",
@ -36,6 +37,7 @@ elif os.name == "posix":
lib_names = [
"gdal",
"GDAL",
"gdal3.9.0",
"gdal3.8.0",
"gdal3.7.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)
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)
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.
add_geom = void_output(lgdal.OGR_G_AddGeometry, [c_void_p, c_void_p])

View File

@ -34,6 +34,18 @@ else:
__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):
pass
@ -106,7 +118,7 @@ class GeoIP2:
)
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}")
def __del__(self):
@ -123,6 +135,14 @@ class GeoIP2:
def _metadata(self):
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):
if not isinstance(query, (str, ipaddress.IPv4Address, ipaddress.IPv6Address)):
raise TypeError(
@ -130,9 +150,7 @@ class GeoIP2:
"IPv6Address, not type %s" % type(query).__name__,
)
is_city = self._metadata.database_type.endswith("City")
if require_city and not is_city:
if require_city and not self.is_city:
raise GeoIP2Exception(f"Invalid GeoIP city data file: {self._path}")
try:
@ -141,7 +159,7 @@ class GeoIP2:
# GeoIP2 only takes IP addresses, so try to resolve a hostname.
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)
def city(self, query):

View File

@ -279,14 +279,14 @@ class Command(BaseCommand):
try:
# When was the target file modified last time?
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
pass
else:
try:
# When was the source file modified last time?
source_last_modified = source_storage.get_modified_time(path)
except (OSError, NotImplementedError, AttributeError):
except (OSError, NotImplementedError):
pass
else:
# The full path of the target file

View File

@ -166,5 +166,5 @@ class FileBasedCache(BaseCache):
"""
return [
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 django.core.checks.async_checks # 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.database # 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"
async_support = "async_support"
caches = "caches"
commands = "commands"
compatibility = "compatibility"
database = "database"
files = "files"

View File

@ -24,6 +24,7 @@ from django.db.migrations.writer import MigrationWriter
class Command(BaseCommand):
autodetector = MigrationAutodetector
help = "Creates new migration(s) for apps."
def add_arguments(self, parser):
@ -209,7 +210,7 @@ class Command(BaseCommand):
log=self.log,
)
# Set up autodetector
autodetector = MigrationAutodetector(
autodetector = self.autodetector(
loader.project_state(),
ProjectState.from_apps(apps),
questioner,
@ -461,7 +462,7 @@ class Command(BaseCommand):
# If they still want to merge it, then write out an empty
# file depending on the migrations needing merging.
numbers = [
MigrationAutodetector.parse_number(migration.name)
self.autodetector.parse_number(migration.name)
for migration in merge_migrations
]
try:

View File

@ -15,6 +15,7 @@ from django.utils.text import Truncator
class Command(BaseCommand):
autodetector = MigrationAutodetector
help = (
"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.")
# If there's changes that aren't in migrations yet, tell them
# how to fix it.
autodetector = MigrationAutodetector(
autodetector = self.autodetector(
executor.loader.project_state(),
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 datetime
import decimal
import yaml
@ -12,7 +13,6 @@ import yaml
from django.core.serializers.base import DeserializationError
from django.core.serializers.python import Deserializer as PythonDeserializer
from django.core.serializers.python import Serializer as PythonSerializer
from django.db import models
# Use the C (faster) implementation if possible
try:
@ -44,17 +44,17 @@ class Serializer(PythonSerializer):
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
# types (as opposed to dates or datetimes, which it does support). Since
# we want to use the "safe" serializer for better interoperability, we
# 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
# halt deserialization under any other language.
if isinstance(field, models.TimeField) and getattr(obj, field.name) is not None:
self._current[field.name] = str(getattr(obj, field.name))
else:
super().handle_field(obj, field)
value = super()._value_from_field(obj, field)
if isinstance(value, datetime.time):
value = str(value)
return value
def end_serialization(self):
self.options.setdefault("allow_unicode", True)

View File

@ -101,13 +101,16 @@ class DomainNameValidator(RegexValidator):
if self.accept_idna:
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:
self.regex = _lazy_re_compile(
self.ascii_only_hostname_re
r"^"
+ self.ascii_only_hostname_re
+ self.ascii_only_domain_re
+ self.ascii_only_tld_re,
+ self.ascii_only_tld_re
+ r"$",
re.IGNORECASE,
)
super().__init__(**kwargs)

View File

@ -215,7 +215,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
def get_connection_params(self):
kwargs = {
"conv": django_conversions,
"charset": "utf8",
"charset": "utf8mb4",
}
settings_dict = self.settings_dict
if settings_dict["USER"]:

View File

@ -71,21 +71,11 @@ class DatabaseFeatures(BaseDatabaseFeatures):
@cached_property
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 {
"ci": f"{charset}_general_ci",
"non_default": f"{charset}_esperanto_ci",
"swedish_ci": f"{charset}_swedish_ci",
"virtual": f"{charset}_esperanto_ci",
"ci": "utf8mb4_general_ci",
"non_default": "utf8mb4_esperanto_ci",
"swedish_ci": "utf8mb4_swedish_ci",
"virtual": "utf8mb4_esperanto_ci",
}
test_now_utc_template = "UTC_TIMESTAMP(6)"
@ -99,10 +89,6 @@ class DatabaseFeatures(BaseDatabaseFeatures):
"db_functions.comparison.test_least.LeastTests."
"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 "
"returns JSON": {
"schema.tests.SchemaTests.test_func_index_json_key_transform",

View File

@ -160,6 +160,10 @@ class DatabaseFeatures(BaseDatabaseFeatures):
def is_postgresql_16(self):
return self.connection.pg_version >= 160000
@cached_property
def is_postgresql_17(self):
return self.connection.pg_version >= 170000
supports_unlimited_charfield = True
supports_nulls_distinct_unique_constraints = property(
operator.attrgetter("is_postgresql_15")

View File

@ -32,7 +32,9 @@ class DatabaseOperations(BaseDatabaseOperations):
"BUFFERS",
"COSTS",
"GENERIC_PLAN",
"MEMORY",
"SETTINGS",
"SERIALIZE",
"SUMMARY",
"TIMING",
"VERBOSE",
@ -365,6 +367,9 @@ class DatabaseOperations(BaseDatabaseOperations):
def explain_query_prefix(self, format=None, **options):
extra = {}
if serialize := options.pop("serialize", None):
if serialize.upper() in {"TEXT", "BINARY"}:
extra["SERIALIZE"] = serialize.upper()
# Normalize options.
if options:
options = {

View File

@ -140,6 +140,13 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
return sequence["name"]
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(
self, model, old_field, new_field, new_type, old_collation, new_collation
):
@ -147,11 +154,7 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
# different type.
old_db_params = old_field.db_parameters(connection=self.connection)
old_type = old_db_params["type"]
if (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"))
):
if self._is_changing_type_of_indexed_text_column(old_field, old_type, new_type):
index_name = self._create_index_name(
model._meta.db_table, [old_field.column], suffix="_like"
)
@ -277,8 +280,14 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
strict,
)
# Added an index? Create any PostgreSQL-specific indexes.
if (not (old_field.db_index or old_field.unique) and new_field.db_index) or (
not old_field.unique and new_field.unique
if (
(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)
if like_index_statement is not None:

View File

@ -101,7 +101,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
"servers.tests.LiveServerTestCloseConnectionTest."
"test_closes_connections",
},
"For SQLite in-memory tests, closing the connection destroys"
"For SQLite in-memory tests, closing the connection destroys "
"the database.": {
"test_utils.tests.AssertNumQueriesUponConnectionTests."
"test_ignores_connection_configuration_queries",

View File

@ -219,6 +219,7 @@ class MigrationAutodetector:
self.generate_altered_unique_together()
self.generate_added_indexes()
self.generate_added_constraints()
self.generate_altered_constraints()
self.generate_altered_db_table()
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):
option_name = operations.AddConstraint.option_name
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]
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(
{
(app_label, model_name): {
"added_constraints": add_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
def _get_dependencies_for_foreign_key(app_label, model_name, field, project_state):
remote_field_model = None

View File

@ -2,6 +2,7 @@ from .fields import AddField, AlterField, RemoveField, RenameField
from .models import (
AddConstraint,
AddIndex,
AlterConstraint,
AlterIndexTogether,
AlterModelManagers,
AlterModelOptions,
@ -36,6 +37,7 @@ __all__ = [
"RenameField",
"AddConstraint",
"RemoveConstraint",
"AlterConstraint",
"SeparateDatabaseAndState",
"RunSQL",
"RunPython",

View File

@ -1230,6 +1230,12 @@ class AddConstraint(IndexOperation):
and self.constraint.name == operation.name
):
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)
@ -1274,3 +1280,51 @@ class RemoveConstraint(IndexOperation):
@property
def migration_name_fragment(self):
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:
try:
return eval(code, {}, {"datetime": datetime, "timezone": timezone})
except (SyntaxError, NameError) as e:
self.prompt_output.write("Invalid input: %s" % e)
except Exception as e:
self.prompt_output.write(f"{e.__class__.__name__}: {e}")
def ask_not_null_addition(self, field_name, model_name):
"""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]
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):
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):
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):
# If preserve default is off, don't use the default for future state.
if not preserve_default:

View File

@ -23,6 +23,8 @@ class BaseConstraint:
violation_error_code = None
violation_error_message = None
non_db_attrs = ("violation_error_code", "violation_error_message")
# RemovedInDjango60Warning: When the deprecation ends, replace with:
# def __init__(
# self, *, name, violation_error_code=None, violation_error_message=None

View File

@ -392,7 +392,10 @@ class Field(RegisterLookupMixin):
if (
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
):
return []

View File

@ -2,7 +2,7 @@ import itertools
from django.core.exceptions import EmptyResultSet
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 (
Exact,
GreaterThan,
@ -12,6 +12,7 @@ from django.db.models.lookups import (
LessThan,
LessThanOrEqual,
)
from django.db.models.sql import Query
from django.db.models.sql.where import AND, OR, WhereNode
@ -28,17 +29,32 @@ class Tuple(Func):
class TupleLookupMixin:
def get_prep_lookup(self):
self.check_rhs_is_tuple_or_list()
self.check_rhs_length_equals_lhs_length()
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):
len_lhs = len(self.lhs)
if len_lhs != len(self.rhs):
lhs_str = self.get_lhs_str()
raise ValueError(
f"'{self.lookup_name}' lookup of '{self.lhs.field.name}' field "
f"must have {len_lhs} elements"
f"{self.lookup_name!r} lookup of {lhs_str} 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):
if isinstance(self.lhs, (tuple, list)):
return Tuple(*self.lhs)
@ -196,17 +212,52 @@ class TupleLessThanOrEqual(TupleLookupMixin, LessThanOrEqual):
class TupleIn(TupleLookupMixin, In):
def get_prep_lookup(self):
self.check_rhs_elements_length_equals_lhs_length()
return super(TupleLookupMixin, self).get_prep_lookup()
if self.rhs_is_direct_value():
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):
len_lhs = len(self.lhs)
if not all(len_lhs == len(vals) for vals in self.rhs):
lhs_str = self.get_lhs_str()
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"
)
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):
rhs = self.rhs
if not rhs:
@ -229,10 +280,17 @@ class TupleIn(TupleLookupMixin, In):
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):
rhs = self.rhs
if not rhs:
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:
# 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)
def as_subquery(self, compiler, connection):
return compiler.compile(In(self.lhs, self.rhs))
tuple_lookups = {
"exact": TupleExact,

View File

@ -160,39 +160,43 @@ class JSONObject(Func):
)
return super().as_sql(compiler, connection, **extra_context)
def as_native(self, compiler, connection, *, returning, **extra_context):
class ArgJoiner:
def join(self, args):
pairs = zip(args[::2], args[1::2], strict=True)
return ", ".join([" VALUE ".join(pair) for pair in pairs])
def join(self, args):
pairs = zip(args[::2], args[1::2], strict=True)
# Wrap 'key' in parentheses in case of postgres cast :: syntax.
return ", ".join([f"({key}) VALUE {value}" for key, value in pairs])
def as_native(self, compiler, connection, *, returning, **extra_context):
return self.as_sql(
compiler,
connection,
arg_joiner=ArgJoiner(),
arg_joiner=self,
template=f"%(function)s(%(expressions)s RETURNING {returning})",
**extra_context,
)
def as_postgresql(self, compiler, connection, **extra_context):
if (
not connection.features.is_postgresql_16
or connection.features.uses_server_side_binding
):
copy = self.copy()
copy.set_source_expressions(
[
Cast(expression, TextField()) if index % 2 == 0 else expression
for index, expression in enumerate(copy.get_source_expressions())
]
# Casting keys to text is only required when using JSONB_BUILD_OBJECT
# or when using JSON_OBJECT on PostgreSQL 16+ with server-side bindings.
# This is done in all cases for consistency.
copy = self.copy()
copy.set_source_expressions(
[
Cast(expression, TextField()) if index % 2 == 0 else expression
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,
connection,
function="JSONB_BUILD_OBJECT",
**extra_context,
)
return self.as_native(compiler, connection, returning="JSONB", **extra_context)
return super(JSONObject, copy).as_sql(
compiler,
connection,
function="JSONB_BUILD_OBJECT",
**extra_context,
)
def as_oracle(self, compiler, connection, **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)
return obj
create.alters_data = True
async def acreate(self, **kwargs):
return await sync_to_async(self.create)(**kwargs)
acreate.alters_data = True
def _prepare_for_bulk_create(self, objs):
from django.db.models.expressions import DatabaseDefault
@ -835,6 +839,8 @@ class QuerySet(AltersData):
return objs
bulk_create.alters_data = True
async def abulk_create(
self,
objs,
@ -853,6 +859,8 @@ class QuerySet(AltersData):
unique_fields=unique_fields,
)
abulk_create.alters_data = True
def bulk_update(self, objs, fields, batch_size=None):
"""
Update the given fields in each of the given objects in the database.
@ -941,12 +949,16 @@ class QuerySet(AltersData):
pass
raise
get_or_create.alters_data = True
async def aget_or_create(self, defaults=None, **kwargs):
return await sync_to_async(self.get_or_create)(
defaults=defaults,
**kwargs,
)
aget_or_create.alters_data = True
def update_or_create(self, defaults=None, create_defaults=None, **kwargs):
"""
Look up an object with the given kwargs, updating one with defaults
@ -992,6 +1004,8 @@ class QuerySet(AltersData):
obj.save(using=self.db)
return obj, False
update_or_create.alters_data = True
async def aupdate_or_create(self, defaults=None, create_defaults=None, **kwargs):
return await sync_to_async(self.update_or_create)(
defaults=defaults,
@ -999,6 +1013,8 @@ class QuerySet(AltersData):
**kwargs,
)
aupdate_or_create.alters_data = True
def _extract_model_params(self, defaults, **kwargs):
"""
Prepare `params` for creating a model instance based on the given

View File

@ -1021,11 +1021,21 @@ class Query(BaseExpression):
if alias == old_alias:
table_aliases[pos] = new_alias
break
# 3. Rename the direct external aliases and the ones of combined
# queries (union, intersection, difference).
self.external_aliases = {
# Table is aliased or it's being changed and thus is aliased.
change_map.get(alias, alias): (aliased or alias in change_map)
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):
"""

View File

@ -570,7 +570,12 @@ def formset_factory(
"validate_max": validate_max,
"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):

View File

@ -21,6 +21,7 @@ from django.http.cookie import SimpleCookie
from django.utils import timezone
from django.utils.datastructures import CaseInsensitiveMapping
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.regex_helper import _lazy_re_compile
@ -408,6 +409,11 @@ class HttpResponse(HttpResponseBase):
content = self.make_bytes(value)
# Create a list of properly encoded bytestrings to support write().
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):
return iter(self._container)
@ -460,6 +466,12 @@ class StreamingHttpResponse(HttpResponseBase):
"`streaming_content` instead." % self.__class__.__name__
)
@property
def text(self):
raise AttributeError(
"This %s instance has no `text` attribute." % self.__class__.__name__
)
@property
def streaming_content(self):
if self.is_async:
@ -615,10 +627,12 @@ class FileResponse(StreamingHttpResponse):
class HttpResponseRedirectBase(HttpResponse):
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)
self["Location"] = iri_to_uri(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:
raise DisallowedRedirect(
"Unsafe redirect to URL with protocol '%s'" % parsed.scheme
@ -640,10 +654,12 @@ class HttpResponseRedirectBase(HttpResponse):
class HttpResponseRedirect(HttpResponseRedirectBase):
status_code = 302
status_code_preserve_request = 307
class HttpResponsePermanentRedirect(HttpResponseRedirectBase):
status_code = 301
status_code_preserve_request = 308
class HttpResponseNotModified(HttpResponse):

View File

@ -26,7 +26,7 @@ def render(
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
passed.
@ -40,13 +40,17 @@ def redirect(to, *args, permanent=False, **kwargs):
* A URL, which will be used as-is for the redirect location.
Issues a temporary redirect by default; pass permanent=True to issue a
permanent redirect.
Issues a temporary redirect by default. Set permanent=True to issue a
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 = (
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):

View File

@ -57,7 +57,7 @@ from enum import Enum
from django.template.context import BaseContext
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.safestring import SafeData, SafeString, mark_safe
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)):
if start >= upto and end <= next:
line = num
before = escape(self.source[upto:start])
during = escape(self.source[start:end])
after = escape(self.source[end:next])
source_lines.append((num, escape(self.source[upto:next])))
before = self.source[upto:start]
during = self.source[start:end]
after = self.source[end:next]
source_lines.append((num, self.source[upto:next]))
upto = next
total = len(source_lines)

View File

@ -37,7 +37,9 @@ class BaseContext:
self.dicts.append(value)
def __copy__(self):
duplicate = copy(super())
duplicate = BaseContext()
duplicate.__class__ = self.__class__
duplicate.__dict__ = copy(self.__dict__)
duplicate.dicts = self.dicts[:]
return duplicate

View File

@ -153,6 +153,90 @@ class Library:
else:
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):
"""
Register a callable as an inclusion tag:
@ -243,6 +327,23 @@ class SimpleNode(TagHelperNode):
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):
def __init__(self, func, takes_context, args, kwargs, filename):
super().__init__(func, takes_context, args, kwargs)

View File

@ -947,9 +947,7 @@ class ClientMixin:
'Content-Type header is "%s", not "application/json"'
% response.get("Content-Type")
)
response._json = json.loads(
response.content.decode(response.charset), **extra
)
response._json = json.loads(response.text, **extra)
return response._json
def _follow_redirect(

View File

@ -12,6 +12,7 @@ import random
import sys
import textwrap
import unittest
import unittest.suite
from collections import defaultdict
from contextlib import contextmanager
from importlib import import_module
@ -292,7 +293,15 @@ failure and get a correct traceback.
def addError(self, 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)
def addFailure(self, test, err):
@ -547,18 +556,32 @@ class ParallelTestSuite(unittest.TestSuite):
tests = list(self.subsuites[subsuite_index])
for event in events:
event_name = event[0]
handler = getattr(result, event_name, None)
if handler is None:
continue
test = tests[event[1]]
args = event[2:]
handler(test, *args)
self.handle_event(result, tests, event)
pool.join()
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):
return iter(self.subsuites)

View File

@ -19,6 +19,7 @@ PY310 = sys.version_info >= (3, 10)
PY311 = sys.version_info >= (3, 11)
PY312 = sys.version_info >= (3, 12)
PY313 = sys.version_info >= (3, 13)
PY314 = sys.version_info >= (3, 14)
def get_version(version=None):

View File

@ -300,7 +300,11 @@ class DateMixin:
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
date_list_period = "year"
@ -388,7 +392,9 @@ class BaseDateListView(MultipleObjectMixin, DateMixin, View):
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"
@ -411,7 +417,11 @@ class ArchiveIndexView(MultipleObjectTemplateResponseMixin, BaseArchiveIndexView
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"
make_object_list = False
@ -463,7 +473,11 @@ class YearArchiveView(MultipleObjectTemplateResponseMixin, BaseYearArchiveView):
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"
@ -505,7 +519,11 @@ class MonthArchiveView(MultipleObjectTemplateResponseMixin, BaseMonthArchiveView
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):
"""Return (date_list, items, extra_context) for this request."""
@ -563,7 +581,11 @@ class WeekArchiveView(MultipleObjectTemplateResponseMixin, BaseWeekArchiveView):
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):
"""Return (date_list, items, extra_context) for this request."""
@ -610,7 +632,11 @@ class DayArchiveView(MultipleObjectTemplateResponseMixin, 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):
"""Return (date_list, items, extra_context) for this request."""
@ -625,8 +651,10 @@ class TodayArchiveView(MultipleObjectTemplateResponseMixin, BaseTodayArchiveView
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.
This requires subclassing to provide a response mixin.
"""
def get_object(self, queryset=None):

View File

@ -102,7 +102,11 @@ class SingleObjectMixin(ContextMixin):
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):
self.object = self.get_object()

View File

@ -170,7 +170,7 @@ class BaseCreateView(ModelFormMixin, ProcessFormView):
"""
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):
@ -194,7 +194,7 @@ class BaseUpdateView(ModelFormMixin, ProcessFormView):
"""
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):
@ -242,7 +242,7 @@ class BaseDeleteView(DeletionMixin, FormMixin, BaseDetailView):
"""
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

View File

@ -148,7 +148,11 @@ class MultipleObjectMixin(ContextMixin):
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):
self.object_list = self.get_queryset()

View File

@ -212,7 +212,7 @@
{% endif %}
{% if frames %}
<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 %}
</h2>
<div id="browserTraceback">

View File

@ -8,6 +8,7 @@ SPHINXBUILD ?= sphinx-build
PAPER ?=
BUILDDIR ?= _build
LANGUAGE ?=
JOBS ?= auto
# Set the default language.
ifndef LANGUAGE
@ -21,7 +22,7 @@ LANGUAGEOPT = $(firstword $(subst _, ,$(LANGUAGE)))
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
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
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
@ -61,7 +62,7 @@ html:
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
htmlview: html
$(PYTHON) -c "import webbrowser; webbrowser.open('_build/html/index.html')"
$(PYTHON) -m webbrowser "$(BUILDDIR)/html/index.html"
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml

View File

@ -13,6 +13,8 @@ import functools
import sys
from os.path import abspath, dirname, join
from sphinx import version_info as sphinx_version
# Workaround for sphinx-build recursion limit overflow:
# pickle.dump(doctree, f, pickle.HIGHEST_PROTOCOL)
# RuntimeError: maximum recursion depth exceeded while pickling an object
@ -138,13 +140,15 @@ django_next_version = "5.2"
extlinks = {
"bpo": ("https://bugs.python.org/issue?@action=redirect&bpo=%s", "bpo-%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"),
# A file or directory. GitHub redirects from blob to tree if needed.
"source": ("https://github.com/django/django/blob/main/%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
# for a list of supported languages.
# 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
sort of attention you require.
Gentle IRC reminders can also work -- again, strategically timed if possible.
During a bug sprint would be a very good time, for example.
Gentle reminders in the ``#contributing-getting-started`` channel in the
`Django Discord server`_ can work.
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
@ -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
get your issue addressed.
.. _`Django Discord server`: https://discord.gg/xcRH6mN4fa
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
Django applications. This type of authentication solution is typically seen on
intranet sites, with single sign-on solutions such as IIS and Integrated
Windows Authentication or Apache and `mod_authnz_ldap`_, `CAS`_, `Cosign`_,
`WebAuth`_, `mod_auth_sspi`_, etc.
Windows Authentication or Apache and `mod_authnz_ldap`_, `CAS`_, `WebAuth`_,
`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
.. _Cosign: http://weblogin.org
.. _WebAuth: https://uit.stanford.edu/service/authentication
.. _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 %}
<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:
Inclusion tags

View File

@ -17,7 +17,7 @@ You can install Hypercorn with ``pip``:
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
location of a module containing an ASGI application object, followed
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
<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
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
you quickly accomplish common tasks.
Practical guides covering common tasks and problems.
Models, data and databases
==========================
.. 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::
:maxdepth: 1
@ -13,25 +59,7 @@ you quickly accomplish common tasks.
auth-remote-user
csrf
custom-management-commands
custom-model-fields
custom-lookups
custom-template-backend
custom-template-tags
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::

View File

@ -111,15 +111,15 @@ reimplement the entire template.
For example, you can use this technique to add a custom logo to the
``admin/base_site.html`` template:
.. code-block:: html+django
:caption: ``templates/admin/base_site.html``
.. code-block:: html+django
:caption: ``templates/admin/base_site.html``
{% extends "admin/base_site.html" %}
{% extends "admin/base_site.html" %}
{% block branding %}
<img src="link/to/logo.png" alt="logo">
{{ block.super }}
{% endblock %}
{% block branding %}
<img src="link/to/logo.png" alt="logo">
{{ block.super }}
{% endblock %}
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
steps: run the :djadmin:`collectstatic` command when static files change, then
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
manually or the :func:`post_process
<django.contrib.staticfiles.storage.StaticFilesStorage.post_process>` method of

View File

@ -232,47 +232,47 @@
</g>
<g id="Graphic_89">
<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"/>
<text transform="translate(193 150)" fill="#797979">
<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="#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="#797979" x="1.2792969" y="39">enough information, or can&apos;t be reproduced.</tspan>
<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="#595959">
<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="#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="#595959" x="1.2792969" y="39">enough information, or can&apos;t be reproduced.</tspan>
</text>
</g>
<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 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 id="Graphic_96">
<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"/>
<text transform="translate(76 150)" fill="#797979">
<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="#797979" 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>
<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="#595959">
<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="#595959" x="4.463867" y="25">bug and should </tspan>
<tspan font-family="Helvetica" font-size="12" fill="#595959" x="22.81836" y="39">be fixed.</tspan>
</text>
</g>
<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 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 id="Graphic_102">
<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"/>
<text transform="translate(76 526)" fill="#797979">
<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="#797979" x="26.591797" y="25">needed tests and docs. A merger can commit it as is.</tspan>
<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="#595959">
<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="#595959" x="26.591797" y="25">needed tests and docs. A merger can commit it as is.</tspan>
</text>
</g>
<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 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 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"/>

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
.. _#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/
.. _Django Discord server: https://discord.gg/xcRH6mN4fa
.. _Django forum: https://forum.djangoproject.com/

View File

@ -21,53 +21,55 @@ First steps
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
can reproduce it and it seems valid, make a note that you confirmed the bug
and accept the ticket. Make sure the ticket is filed under the correct
component area. Consider writing a patch that adds a test for the bug's
behavior, even if you don't fix the bug itself. See more at
:ref:`how-can-i-help-with-triaging`
If an `unreviewed ticket`_ reports a bug, try and reproduce it. If you can
reproduce it and it seems valid, make a note that you confirmed the bug and
accept the ticket. Make sure the ticket is filed under the correct component
area. Consider writing a patch that adds a test for the bug's behavior, even if
you don't fix the bug itself. See more at :ref:`how-can-i-help-with-triaging`
* **Look for tickets that are accepted and review patches to build familiarity
with the codebase and the process**
Review patches of accepted tickets
----------------------------------
Mark the appropriate flags if a patch needs docs or tests. Look through the
changes a patch makes, and keep an eye out for syntax that is incompatible
with older but still supported versions of Python. :doc:`Run the tests
</internals/contributing/writing-code/unit-tests>` and make sure they pass.
Where possible and relevant, try them out on a database other than SQLite.
Leave comments and feedback!
This will help you build familiarity with the codebase and processes. Mark the
appropriate flags if a patch needs docs or tests. Look through the changes a
patch makes, and keep an eye out for syntax that is incompatible with older but
still supported versions of Python. :doc:`Run the tests
</internals/contributing/writing-code/unit-tests>` and make sure they pass.
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
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
:doc:`writing-code/submitting-patches`.
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
expected. Updating a patch is both useful and important! See more on
: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
a typo? Do you think that something should be clarified? Go ahead and
suggest a documentation patch! See also the guide on
:doc:`writing-documentation`.
Django's documentation is great but it can always be improved. Did you find a
typo? Do you think that something should be clarified? Go ahead and suggest a
documentation patch! See also the guide on :doc:`writing-documentation`.
.. note::
.. note::
The `reports page`_ contains links to many useful Trac queries, including
several that are useful for triaging tickets and reviewing patches as
suggested above.
The `reports page`_ contains links to many useful Trac queries, including
several that are useful for triaging tickets and reviewing patches as
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
contribution is more than one or two lines of code, you need to sign the
`CLA`_. See the `Contributor License Agreement FAQ`_ for a more thorough
explanation.
The code that you write belongs to you or your employer. If your contribution
is more than one or two lines of code, you need to sign the `CLA`_. See the
`Contributor License Agreement FAQ`_ for a more thorough explanation.
.. _CLA: https://www.djangoproject.com/foundation/cla/
.. _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
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
that you want to learn about**
Pick a subject area
-------------------
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.
This should be something that you care about, that you are familiar with or
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.
When reading Trac, you need to take into account who says things, and when
they were said. Support for an idea two years ago doesn't necessarily mean
that the idea will still have support. You also need to pay attention to who
*hasn't* spoken -- for example, if an experienced contributor hasn't been
recently involved in a discussion, then a ticket may not have the support
required to get into Django.
Trac isn't an absolute; the context is just as important as the words. When
reading Trac, you need to take into account who says things, and when they were
said. Support for an idea two years ago doesn't necessarily mean that the idea
will still have support. You also need to pay attention to who *hasn't* spoken
-- for example, if an experienced contributor hasn't been recently involved in
a discussion, then a ticket may not have the support required to get into
Django.
* **Start small**
Start small
-----------
It's easier to get feedback on a little issue than on a big one. See the
`easy pickings`_.
It's easier to get feedback on a little issue than on a big one. See the
`easy pickings`_.
* **If you're going to engage in a big task, make sure that your idea has
support first**
Confirm support before engaging in a big task
---------------------------------------------
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
you go implementing it.
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 you
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
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
ultimately have a much greater impact than that of any one person. We can't
do it without **you**!
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
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 do
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
such. Leave a comment instead, letting others know your thoughts. If you're
mostly certain, but not completely certain, you might also try asking on IRC
to see if someone else can confirm your suspicions.
If you're really not certain if a ticket is ready, don't mark it as such. Leave
a comment instead, letting others know your thoughts. If you're mostly certain,
but not completely certain, you might also try asking on the
``#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
repeat. The shotgun approach of taking on lots of tickets and letting some
fall by the wayside ends up doing more harm than good.
Wait for feedback, and respond to feedback that you receive
-----------------------------------------------------------
* **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
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 rigorous
-----------
* **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.
This isn't personal. There are a lot of tickets and pull requests to get
through.
Be patient
----------
Keeping your patch up to date is important. Review the ticket on Trac to
ensure that the *Needs tests*, *Needs documentation*, and *Patch needs
improvement* flags are unchecked once you've addressed all review comments.
It's not always easy for your ticket or your patch to be reviewed quickly. This
isn't personal. There are a lot of tickets and pull requests to get through.
Remember that Django has an eight-month release cycle, so there's plenty of
time for your patch to be reviewed.
Keeping your patch up to date is important. Review the ticket on Trac to ensure
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
<new-contributors-faq>` for ideas here.
Remember that Django has an eight-month release cycle, so there's plenty of
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

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:
.. image:: /internals/_images/triage_process.*
:height: 501
:width: 400
:height: 750
:width: 600
:alt: Django's ticket triage workflow
We've got two roles in this diagram:

View File

@ -417,7 +417,7 @@ Model style
* All database fields
* Custom manager attributes
* ``class Meta``
* ``def __str__()``
* ``def __str__()`` and other Python magic methods
* ``def save()``
* ``def get_absolute_url()``
* 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.
* 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
"Action" near the bottom of the page, then click "Submit changes."
* Claim the ticket by clicking the "assign to" radio button in the "Action"
section. Your username will be filled in the text box by default.
* Finally click the "Submit changes" button at the bottom to save.
.. note::
The Django software foundation requests that anyone contributing more than
@ -114,7 +118,7 @@ requirements:
feature, the change should also contain documentation.
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
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/
.. _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
change that introduces new Django functionality and makes some sort of design
decision.
A wider community discussion is required when a patch introduces new Django
functionality and makes some sort of design decision. This is especially
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
been discussed on the `Django Forum`_ or |django-developers| list.
The following are different approaches for gaining feedback from the community.
If you're not sure whether your contribution should be considered non-trivial,
ask on the ticket for opinions.
The Django Forum or django-developers mailing list
--------------------------------------------------
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 Enhancement Proposals: https://github.com/django/deps
.. _deprecating-a-feature:

View File

@ -322,7 +322,6 @@ dependencies:
* :pypi:`numpy`
* :pypi:`Pillow` 6.2.1+
* :pypi:`PyYAML`
* :pypi:`pytz` (required)
* :pypi:`pywatchman`
* :pypi:`redis` 3.4+
* :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.
You'll need to install :pypi:`sphinxcontrib-spelling` first. Then from the
``docs`` directory, run ``make spelling``. Wrong words (if any) along with the
file and line number where they occur will be saved to
``_build/spelling/output.txt``.
``docs`` directory, run:
.. 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
one of the following:
@ -179,10 +184,21 @@ Link check
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
links in the documentation are working. From the ``docs`` directory, run ``make
linkcheck``. Output is printed to the terminal, but can also be found in
links in the documentation are working. From the ``docs`` directory, run:
.. console::
$ make linkcheck
Output is printed to the terminal, but can also be found in
``_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
"ignored" have been skipped because they either cannot be checked or have
matched ignore rules in the configuration.
@ -290,7 +306,8 @@ documentation:
display a link with the title "auth".
* 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'
documentation.
@ -324,8 +341,9 @@ documentation:
Five
^^^^
* Use :rst:role:`:rfc:<rfc>` to reference RFC and try to link to the relevant
section if possible. For example, use ``:rfc:`2324#section-2.3.2``` or
* Use :rst:role:`:rfc:<rfc>` to reference a Request for Comments (RFC) and
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>```.
* 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
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
======================
@ -518,7 +539,7 @@ Minimizing images
Optimize image compression where possible. For PNG files, use OptiPNG and
AdvanceCOMP's ``advpng``:
.. code-block:: console
.. console::
$ cd docs
$ 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
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
``docs`` directory. The new man page will be written in
``docs/_build/man/django-admin.1``.
To generate an updated version of the man page, in the ``docs`` directory, run:
.. 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()``
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:
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
began if possible.
#. Remove ``.. versionadded::``, ``.. versionadded::``, and ``.. deprecated::``
annotations in the documentation from two releases ago. For example, in
Django 4.2, notes for 4.0 will be removed.
#. Remove ``.. versionadded::``, ``.. versionchanged::``, and
``.. deprecated::`` annotations in the documentation from two releases ago.
For example, in Django 4.2, notes for 4.0 will be removed.
#. Add the new branch to `Read the Docs
<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
.. _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:
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
encounter.
Now we are ready to run the test suite. If you're using GNU/Linux, macOS, or
some other flavor of Unix, run:
Now we are ready to run the test suite:
.. 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``
{% load static %}
<html>
<html lang="en">
<head>
<title>{% block title %}{% endblock %}</title>
</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
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.
Reusability matters

View File

@ -22,10 +22,11 @@ Installing Django Debug Toolbar
===============================
Django Debug Toolbar is a useful tool for debugging Django web applications.
It's a third-party package maintained by the `Jazzband
<https://jazzband.co>`_ organization. The toolbar helps you understand how your
application functions and to identify problems. It does so by providing panels
that provide debug information about the current request and response.
It's a third-party package that is maintained by the community organization
`Django Commons <https://github.com/django-commons>`_. The toolbar helps you
understand how your application functions and to identify problems. It does so
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
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
outlines troubleshooting options.
#. 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/>`_.
#. Join the `Django Discord server <https://discord.gg/xcRH6mN4fa>`_.
#. 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.
* ``caches``: Checks cache related configuration.
* ``compatibility``: Flags potential problems with version upgrades.
* ``commands``: Checks custom management commands related configuration.
* ``database``: Checks database-related configuration issues. Database checks
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
@ -428,6 +429,14 @@ Models
* **models.W047**: ``<database>`` does not support unique constraints with
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
--------

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
upper right of the page.
.. _admindocs-helpers:
Documentation helpers
=====================
@ -47,13 +49,23 @@ Template filters ``:filter:`filtername```
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
===============
The **models** section of the ``admindocs`` page describes each model in the
system along with all the fields, properties, and methods available on it.
Relationships to other models appear as hyperlinks. Descriptions are pulled
from ``help_text`` attributes on fields or from docstrings on model methods.
The **models** section of the ``admindocs`` page describes each model that the
user has access to along with all the fields, properties, and methods available
on it. Relationships to other models appear as hyperlinks. Descriptions are
pulled from ``help_text`` attributes on fields or from docstrings on model
methods.
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."""
...
.. versionchanged:: 5.2
Access was restricted to only allow users with model view or change
permissions.
View reference
==============

View File

@ -337,7 +337,8 @@ subclass::
If neither ``fields`` nor :attr:`~ModelAdmin.fieldsets` options are present,
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
are defined in the model.
are defined in the model, followed by any fields defined in
:attr:`~ModelAdmin.readonly_fields`.
.. attribute:: ModelAdmin.fieldsets
@ -1465,6 +1466,27 @@ templates used by the :class:`ModelAdmin` views:
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)
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``::
from django.contrib import admin
from myapp.models import Author, Book
class BookInline(admin.TabularInline):
@ -2240,6 +2263,9 @@ information.
BookInline,
]
admin.site.register(Author, AuthorAdmin)
Django provides two subclasses of ``InlineModelAdmin`` and they are:
* :class:`~django.contrib.admin.TabularInline`
@ -2472,6 +2498,10 @@ Take this model for instance::
from django.db import models
class Person(models.Model):
name = models.CharField(max_length=128)
class Friendship(models.Model):
to_person = models.ForeignKey(
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::
from django.contrib import admin
from myapp.models import Friendship
from myapp.models import Friendship, Person
class FriendshipInline(admin.TabularInline):
@ -2498,6 +2528,9 @@ automatically::
FriendshipInline,
]
admin.site.register(Person, PersonAdmin)
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::
from django.contrib import admin
from myapp.models import Group
class MembershipInline(admin.TabularInline):
model = Group.members.through
class PersonAdmin(admin.ModelAdmin):
inlines = [
MembershipInline,
]
class GroupAdmin(admin.ModelAdmin):
inlines = [
MembershipInline,
]
exclude = ["members"]
admin.site.register(Group, GroupAdmin)
There are two features worth noting in this example.
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
a longer length, please use a :ref:`custom user model
<specifying-custom-user-model>`. If you use MySQL with the ``utf8mb4``
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.
<specifying-custom-user-model>`.
.. attribute:: first_name
@ -54,7 +51,8 @@ Fields
Required. A hash of, and metadata about, the password. (Django doesn't
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>`.
.. attribute:: groups
@ -175,8 +173,9 @@ Methods
.. method:: set_unusable_password()
Marks the user as having no password set. This isn't the same as
having a blank string for a password.
Marks the user as having no password set by updating the metadata in
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
will never return ``True``. Doesn't save the
:class:`~django.contrib.auth.models.User` object.
@ -420,8 +419,8 @@ fields:
.. attribute:: content_type
Required. A reference to the ``django_content_type`` database table,
which contains a record for each installed model.
Required. A foreign key to the
:class:`~django.contrib.contenttypes.models.ContentType` model.
.. 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
:attr:`~django.contrib.auth.models.AbstractBaseUser.is_anonymous` or
:attr:`~django.contrib.auth.models.CustomUser.is_active` is ``False``.
.. versionchanged:: 5.2
``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
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<title>{{ flatpage.title }}</title>
</head>

Some files were not shown because too many files have changed in this diff Show More