Merge branch 'django:main' into ticket_34034
2
.github/workflows/python_matrix.yml
vendored
@ -49,4 +49,4 @@ jobs:
|
||||
run: python -m pip install --upgrade pip setuptools wheel
|
||||
- run: python -m pip install -r tests/requirements/py3.txt -e .
|
||||
- name: Run tests
|
||||
run: python tests/runtests.py -v2
|
||||
run: python -Wall tests/runtests.py -v2
|
||||
|
5
.github/workflows/schedule_tests.yml
vendored
@ -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
|
||||
|
2
.github/workflows/tests.yml
vendored
@ -38,7 +38,7 @@ jobs:
|
||||
run: python -m pip install --upgrade pip setuptools wheel
|
||||
- run: python -m pip install -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
|
||||
|
@ -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));
|
||||
|
@ -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 %}
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
{% block form_top %}
|
||||
{% if not is_popup %}
|
||||
<p>{% translate "After you've created a user, you’ll be able to edit more user options." %}</p>
|
||||
<p>{% translate "After you’ve created a user, you’ll be able to edit more user options." %}</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% block extrahead %}
|
||||
|
@ -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 %}
|
||||
|
@ -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).
|
||||
|
@ -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 can’t 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 can’t 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 can’t be entirely numeric.")
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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])
|
||||
|
@ -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):
|
||||
|
@ -237,6 +237,11 @@ class CreateCollation(CollationOperation):
|
||||
def migration_name_fragment(self):
|
||||
return "create_collation_%s" % self.name.lower()
|
||||
|
||||
def reduce(self, operation, app_label):
|
||||
if isinstance(operation, RemoveCollation) and self.name == operation.name:
|
||||
return []
|
||||
return super().reduce(operation, app_label)
|
||||
|
||||
|
||||
class RemoveCollation(CollationOperation):
|
||||
"""Remove a collation."""
|
||||
|
@ -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
|
||||
|
2
django/core/cache/backends/filebased.py
vendored
@ -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)
|
||||
]
|
||||
|
@ -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
|
||||
|
28
django/core/checks/commands.py
Normal file
@ -0,0 +1,28 @@
|
||||
from django.core.checks import Error, Tags, register
|
||||
|
||||
|
||||
@register(Tags.commands)
|
||||
def migrate_and_makemigrations_autodetector(**kwargs):
|
||||
from django.core.management import get_commands, load_command_class
|
||||
|
||||
commands = get_commands()
|
||||
|
||||
make_migrations = load_command_class(commands["makemigrations"], "makemigrations")
|
||||
migrate = load_command_class(commands["migrate"], "migrate")
|
||||
|
||||
if make_migrations.autodetector is not migrate.autodetector:
|
||||
return [
|
||||
Error(
|
||||
"The migrate and makemigrations commands must have the same "
|
||||
"autodetector.",
|
||||
hint=(
|
||||
f"makemigrations.Command.autodetector is "
|
||||
f"{make_migrations.autodetector.__name__}, but "
|
||||
f"migrate.Command.autodetector is "
|
||||
f"{migrate.autodetector.__name__}."
|
||||
),
|
||||
id="commands.E001",
|
||||
)
|
||||
]
|
||||
|
||||
return []
|
@ -12,6 +12,7 @@ class Tags:
|
||||
admin = "admin"
|
||||
async_support = "async_support"
|
||||
caches = "caches"
|
||||
commands = "commands"
|
||||
compatibility = "compatibility"
|
||||
database = "database"
|
||||
files = "files"
|
||||
|
@ -1,5 +1,5 @@
|
||||
"""
|
||||
Global Django exception and warning classes.
|
||||
Global Django exception classes.
|
||||
"""
|
||||
|
||||
import operator
|
||||
|
@ -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:
|
||||
|
@ -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),
|
||||
)
|
||||
|
@ -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)
|
||||
|
@ -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")
|
||||
|
@ -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 = {
|
||||
|
@ -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:
|
||||
|
@ -690,11 +690,19 @@ class UniqueConstraint(BaseConstraint):
|
||||
queryset = queryset.exclude(pk=model_class_pk)
|
||||
if not self.condition:
|
||||
if queryset.exists():
|
||||
if self.fields:
|
||||
# When fields are defined, use the unique_error_message() for
|
||||
# backward compatibility.
|
||||
if (
|
||||
self.fields
|
||||
and self.violation_error_message
|
||||
== self.default_violation_error_message
|
||||
):
|
||||
# When fields are defined, use the unique_error_message() as
|
||||
# a default for backward compatibility.
|
||||
validation_error_message = instance.unique_error_message(
|
||||
model, self.fields
|
||||
)
|
||||
raise ValidationError(
|
||||
instance.unique_error_message(model, self.fields),
|
||||
validation_error_message,
|
||||
code=validation_error_message.code,
|
||||
)
|
||||
raise ValidationError(
|
||||
self.get_violation_error_message(),
|
||||
|
@ -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,
|
||||
@ -28,17 +28,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,14 +211,25 @@ class TupleLessThanOrEqual(TupleLookupMixin, LessThanOrEqual):
|
||||
|
||||
class TupleIn(TupleLookupMixin, In):
|
||||
def get_prep_lookup(self):
|
||||
self.check_rhs_is_tuple_or_list()
|
||||
self.check_rhs_is_collection_of_tuples_or_lists()
|
||||
self.check_rhs_elements_length_equals_lhs_length()
|
||||
return super(TupleLookupMixin, self).get_prep_lookup()
|
||||
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"
|
||||
)
|
||||
|
||||
|
@ -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):
|
||||
"""
|
||||
|
@ -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:
|
||||
|
@ -533,9 +533,13 @@ class Parser:
|
||||
def extend_nodelist(self, nodelist, node, token):
|
||||
# Check that non-text nodes don't appear before an extends tag.
|
||||
if node.must_be_first and nodelist.contains_nontext:
|
||||
if self.origin.template_name:
|
||||
origin = repr(self.origin.template_name)
|
||||
else:
|
||||
origin = "the template"
|
||||
raise self.error(
|
||||
token,
|
||||
"%r must be the first tag in the template." % node,
|
||||
"{%% %s %%} must be the first tag in %s." % (token.contents, origin),
|
||||
)
|
||||
if not isinstance(node, TextNode):
|
||||
nodelist.contains_nontext = True
|
||||
|
@ -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(
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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">
|
||||
|
@ -61,7 +61,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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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/
|
||||
|
@ -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::
|
||||
|
||||
|
@ -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:
|
||||
|
||||
|
@ -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't a bug, doesn't contain </tspan>
|
||||
<tspan font-family="Helvetica" font-size="12" fill="#797979" x="1.2792969" y="39">enough information, or can't be reproduced.</tspan>
|
||||
<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't a bug, doesn't contain </tspan>
|
||||
<tspan font-family="Helvetica" font-size="12" fill="#595959" x="1.2792969" y="39">enough information, or can'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 |
@ -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:
|
||||
|
@ -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
|
||||
|
@ -114,7 +114,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 +140,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 Python’s PEPs, Django has `Django Enhancement Proposals`_ or DEPs. A
|
||||
DEP is a design document which provides information to the Django community, or
|
||||
describes a new feature or process for Django. They provide concise technical
|
||||
specifications of features, along with rationales. DEPs are also the primary
|
||||
mechanism for proposing and collecting community input on major new features.
|
||||
|
||||
Before considering writing a DEP, it is recommended to first open a discussion
|
||||
on the `Django Forum`_ or |django-developers| mailing list. This allows the
|
||||
community to provide feedback and helps refine the proposal. Once the DEP is
|
||||
ready the :ref:`Steering Council <steering-council>` votes on whether to accept
|
||||
it.
|
||||
|
||||
Some examples of DEPs that have been approved and fully implemented:
|
||||
|
||||
* `DEP 181: ORM Expressions <https://github.com/django/deps/blob/main/final/0181-orm-expressions.rst>`_
|
||||
* `DEP 182: Multiple Template Engines <https://github.com/django/deps/blob/main/final/0182-multiple-template-engines.rst>`_
|
||||
* `DEP 201: Simplified routing syntax <https://github.com/django/deps/blob/main/final/0201-simplified-routing-syntax.rst>`_
|
||||
|
||||
.. _Django Forum: https://forum.djangoproject.com/
|
||||
.. _Django Enhancement Proposals: https://github.com/django/deps
|
||||
|
||||
.. _deprecating-a-feature:
|
||||
|
||||
|
@ -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``.
|
||||
|
@ -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
|
||||
|
@ -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::
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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 1–7, we encourage you to review
|
||||
If you haven't recently completed Tutorials 1–8, we encourage you to review
|
||||
these so that your example project matches the one described below.
|
||||
|
||||
Reusability matters
|
||||
|
@ -293,7 +293,8 @@ app will still work.
|
||||
.. admonition:: When to use :func:`~django.urls.include()`
|
||||
|
||||
You should always use ``include()`` when you include other URL patterns.
|
||||
``admin.site.urls`` is the only exception to this.
|
||||
The only exception is ``admin.site.urls``, which is a pre-built URLconf
|
||||
provided by Django for the default admin site.
|
||||
|
||||
You have now wired an ``index`` view into the URLconf. Verify it's working with
|
||||
the following command:
|
||||
|
@ -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
|
||||
--------
|
||||
|
||||
|
@ -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
|
||||
|
@ -54,7 +54,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 +176,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.
|
||||
|
@ -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>
|
||||
|
@ -339,42 +339,42 @@ divided into the three categories described in the :ref:`raster lookup details
|
||||
<spatial-lookup-raster>`: native support ``N``, bilateral native support ``B``,
|
||||
and geometry conversion support ``C``.
|
||||
|
||||
================================= ========= ======== ========= ============ ========== ========
|
||||
Lookup Type PostGIS Oracle MariaDB MySQL [#]_ SpatiaLite PGRaster
|
||||
================================= ========= ======== ========= ============ ========== ========
|
||||
:lookup:`bbcontains` X X X X N
|
||||
:lookup:`bboverlaps` X X X X N
|
||||
:lookup:`contained` X X X X N
|
||||
:lookup:`contains <gis-contains>` X X X X X B
|
||||
:lookup:`contains_properly` X B
|
||||
:lookup:`coveredby` X X X B
|
||||
:lookup:`covers` X X X B
|
||||
:lookup:`crosses` X X X X C
|
||||
:lookup:`disjoint` X X X X X B
|
||||
:lookup:`distance_gt` X X X X X N
|
||||
:lookup:`distance_gte` X X X X X N
|
||||
:lookup:`distance_lt` X X X X X N
|
||||
:lookup:`distance_lte` X X X X X N
|
||||
:lookup:`dwithin` X X X B
|
||||
:lookup:`equals` X X X X X C
|
||||
:lookup:`exact <same_as>` X X X X X B
|
||||
:lookup:`intersects` X X X X X B
|
||||
================================= ========= ======== ========== ============ ========== ========
|
||||
Lookup Type PostGIS Oracle MariaDB MySQL [#]_ SpatiaLite PGRaster
|
||||
================================= ========= ======== ========== ============ ========== ========
|
||||
:lookup:`bbcontains` X X X X N
|
||||
:lookup:`bboverlaps` X X X X N
|
||||
:lookup:`contained` X X X X N
|
||||
:lookup:`contains <gis-contains>` X X X X X B
|
||||
:lookup:`contains_properly` X B
|
||||
:lookup:`coveredby` X X X (≥ 11.7) X X B
|
||||
:lookup:`covers` X X X X B
|
||||
:lookup:`crosses` X X X X C
|
||||
:lookup:`disjoint` X X X X X B
|
||||
:lookup:`distance_gt` X X X X X N
|
||||
:lookup:`distance_gte` X X X X X N
|
||||
:lookup:`distance_lt` X X X X X N
|
||||
:lookup:`distance_lte` X X X X X N
|
||||
:lookup:`dwithin` X X X B
|
||||
:lookup:`equals` X X X X X C
|
||||
:lookup:`exact <same_as>` X X X X X B
|
||||
:lookup:`intersects` X X X X X B
|
||||
:lookup:`isempty` X
|
||||
:lookup:`isvalid` X X X X
|
||||
:lookup:`overlaps` X X X X X B
|
||||
:lookup:`relate` X X X X C
|
||||
:lookup:`same_as` X X X X X B
|
||||
:lookup:`touches` X X X X X B
|
||||
:lookup:`within` X X X X X B
|
||||
:lookup:`left` X C
|
||||
:lookup:`right` X C
|
||||
:lookup:`overlaps_left` X B
|
||||
:lookup:`overlaps_right` X B
|
||||
:lookup:`overlaps_above` X C
|
||||
:lookup:`overlaps_below` X C
|
||||
:lookup:`strictly_above` X C
|
||||
:lookup:`strictly_below` X C
|
||||
================================= ========= ======== ========= ============ ========== ========
|
||||
:lookup:`isvalid` X X X (≥ 11.7) X X
|
||||
:lookup:`overlaps` X X X X X B
|
||||
:lookup:`relate` X X X X C
|
||||
:lookup:`same_as` X X X X X B
|
||||
:lookup:`touches` X X X X X B
|
||||
:lookup:`within` X X X X X B
|
||||
:lookup:`left` X C
|
||||
:lookup:`right` X C
|
||||
:lookup:`overlaps_left` X B
|
||||
:lookup:`overlaps_right` X B
|
||||
:lookup:`overlaps_above` X C
|
||||
:lookup:`overlaps_below` X C
|
||||
:lookup:`strictly_above` X C
|
||||
:lookup:`strictly_below` X C
|
||||
================================= ========= ======== ========== ============ ========== ========
|
||||
|
||||
.. _database-functions-compatibility:
|
||||
|
||||
@ -406,10 +406,10 @@ Function PostGIS Oracle MariaDB MySQL
|
||||
:class:`ForcePolygonCW` X X
|
||||
:class:`FromWKB` X X X X X
|
||||
:class:`FromWKT` X X X X X
|
||||
:class:`GeoHash` X X X (LWGEOM/RTTOPO)
|
||||
:class:`GeoHash` X X (≥ 11.7) X X (LWGEOM/RTTOPO)
|
||||
:class:`Intersection` X X X X X
|
||||
:class:`IsEmpty` X
|
||||
:class:`IsValid` X X X X
|
||||
:class:`IsValid` X X X (≥ 11.7) X X
|
||||
:class:`Length` X X X X X
|
||||
:class:`LineLocatePoint` X X
|
||||
:class:`MakeValid` X X (LWGEOM/RTTOPO)
|
||||
@ -431,20 +431,19 @@ Aggregate Functions
|
||||
-------------------
|
||||
|
||||
The following table provides a summary of what GIS-specific aggregate functions
|
||||
are available on each spatial backend. Please note that MariaDB does not
|
||||
support any of these aggregates, and is thus excluded from the table.
|
||||
are available on each spatial backend.
|
||||
|
||||
.. currentmodule:: django.contrib.gis.db.models
|
||||
|
||||
======================= ======= ====== ============ ==========
|
||||
Aggregate PostGIS Oracle MySQL SpatiaLite
|
||||
======================= ======= ====== ============ ==========
|
||||
:class:`Collect` X X (≥ 8.0.24) X
|
||||
:class:`Extent` X X X
|
||||
======================= ======= ====== ========== ============ ==========
|
||||
Aggregate PostGIS Oracle MariaDB MySQL SpatiaLite
|
||||
======================= ======= ====== ========== ============ ==========
|
||||
:class:`Collect` X X (≥ 11.7) X (≥ 8.0.24) X
|
||||
:class:`Extent` X X X
|
||||
:class:`Extent3D` X
|
||||
:class:`MakeLine` X X
|
||||
:class:`Union` X X X
|
||||
======================= ======= ====== ============ ==========
|
||||
:class:`MakeLine` X X
|
||||
:class:`Union` X X X
|
||||
======================= ======= ====== ========== ============ ==========
|
||||
|
||||
.. rubric:: Footnotes
|
||||
.. [#fnwkt] *See* Open Geospatial Consortium, Inc., `OpenGIS Simple Feature Specification For SQL <https://portal.ogc.org/files/?artifact_id=829>`_, Document 99-049 (May 5, 1999), at Ch. 3.2.5, p. 3-11 (SQL Textual Representation of Geometry).
|
||||
|
@ -393,7 +393,7 @@ Creates geometry from `Well-known text (WKT)`_ representation. The optional
|
||||
|
||||
.. class:: GeoHash(expression, precision=None, **extra)
|
||||
|
||||
*Availability*: `MySQL
|
||||
*Availability*: MariaDB, `MySQL
|
||||
<https://dev.mysql.com/doc/refman/en/spatial-geohash-functions.html#function_st-geohash>`__,
|
||||
`PostGIS <https://postgis.net/docs/ST_GeoHash.html>`__, SpatiaLite
|
||||
(LWGEOM/RTTOPO)
|
||||
@ -406,6 +406,10 @@ result.
|
||||
|
||||
__ https://en.wikipedia.org/wiki/Geohash
|
||||
|
||||
.. versionchanged:: 5.2
|
||||
|
||||
MariaDB 11.7+ support was added.
|
||||
|
||||
``GeometryDistance``
|
||||
====================
|
||||
|
||||
@ -444,13 +448,17 @@ geometry. Returns ``True`` if its value is empty and ``False`` otherwise.
|
||||
|
||||
.. class:: IsValid(expr)
|
||||
|
||||
*Availability*: `MySQL
|
||||
*Availability*: MariaDB, `MySQL
|
||||
<https://dev.mysql.com/doc/refman/en/spatial-convenience-functions.html#function_st-isvalid>`__,
|
||||
`PostGIS <https://postgis.net/docs/ST_IsValid.html>`__, Oracle, SpatiaLite
|
||||
|
||||
Accepts a geographic field or expression and tests if the value is well formed.
|
||||
Returns ``True`` if its value is a valid geometry and ``False`` otherwise.
|
||||
|
||||
.. versionchanged:: 5.2
|
||||
|
||||
MariaDB 11.7+ support was added.
|
||||
|
||||
``Length``
|
||||
==========
|
||||
|
||||
|
@ -611,6 +611,26 @@ coordinate transformation:
|
||||
>>> polygon.geom_count
|
||||
1
|
||||
|
||||
.. attribute:: has_curve
|
||||
|
||||
.. versionadded:: 5.2
|
||||
|
||||
A boolean indicating if this geometry is or contains a curve geometry.
|
||||
|
||||
.. method:: get_linear_geometry
|
||||
|
||||
.. versionadded:: 5.2
|
||||
|
||||
Returns a linear version of the geometry. If no conversion can be made, the
|
||||
original geometry is returned.
|
||||
|
||||
.. method:: get_curve_geometry
|
||||
|
||||
.. versionadded:: 5.2
|
||||
|
||||
Returns a curved version of the geometry. If no conversion can be made, the
|
||||
original geometry is returned.
|
||||
|
||||
.. attribute:: point_count
|
||||
|
||||
Returns the number of points used to describe this geometry:
|
||||
|
@ -183,7 +183,7 @@ PostGIS ``ST_ContainsProperly(poly, geom)``
|
||||
-------------
|
||||
|
||||
*Availability*: `PostGIS <https://postgis.net/docs/ST_CoveredBy.html>`__,
|
||||
Oracle, PGRaster (Bilateral), SpatiaLite
|
||||
Oracle, MariaDB 11.7+, MySQL, PGRaster (Bilateral), SpatiaLite
|
||||
|
||||
Tests if no point in the geometry field is outside the lookup geometry.
|
||||
[#fncovers]_
|
||||
@ -197,16 +197,22 @@ Backend SQL Equivalent
|
||||
========== =============================
|
||||
PostGIS ``ST_CoveredBy(poly, geom)``
|
||||
Oracle ``SDO_COVEREDBY(poly, geom)``
|
||||
MariaDB ``MBRCoveredBy(poly, geom)``
|
||||
MySQL ``MBRCoveredBy(poly, geom)``
|
||||
SpatiaLite ``CoveredBy(poly, geom)``
|
||||
========== =============================
|
||||
|
||||
.. versionchanged:: 5.2
|
||||
|
||||
MySQL and MariaDB 11.7+ support was added.
|
||||
|
||||
.. fieldlookup:: covers
|
||||
|
||||
``covers``
|
||||
----------
|
||||
|
||||
*Availability*: `PostGIS <https://postgis.net/docs/ST_Covers.html>`__,
|
||||
Oracle, PGRaster (Bilateral), SpatiaLite
|
||||
Oracle, MySQL, PGRaster (Bilateral), SpatiaLite
|
||||
|
||||
Tests if no point in the lookup geometry is outside the geometry field.
|
||||
[#fncovers]_
|
||||
@ -220,9 +226,14 @@ Backend SQL Equivalent
|
||||
========== ==========================
|
||||
PostGIS ``ST_Covers(poly, geom)``
|
||||
Oracle ``SDO_COVERS(poly, geom)``
|
||||
MySQL ``MBRCovers(poly, geom)``
|
||||
SpatiaLite ``Covers(poly, geom)``
|
||||
========== ==========================
|
||||
|
||||
.. versionchanged:: 5.2
|
||||
|
||||
MySQL support was added.
|
||||
|
||||
.. fieldlookup:: crosses
|
||||
|
||||
``crosses``
|
||||
@ -364,8 +375,8 @@ Example::
|
||||
``isvalid``
|
||||
-----------
|
||||
|
||||
*Availability*: MySQL, `PostGIS <https://postgis.net/docs/ST_IsValid.html>`__,
|
||||
Oracle, SpatiaLite
|
||||
*Availability*: MariaDB, MySQL,
|
||||
`PostGIS <https://postgis.net/docs/ST_IsValid.html>`__, Oracle, SpatiaLite
|
||||
|
||||
Tests if the geometry is valid.
|
||||
|
||||
@ -373,12 +384,16 @@ Example::
|
||||
|
||||
Zipcode.objects.filter(poly__isvalid=True)
|
||||
|
||||
========================== ================================================================
|
||||
Backend SQL Equivalent
|
||||
========================== ================================================================
|
||||
MySQL, PostGIS, SpatiaLite ``ST_IsValid(poly)``
|
||||
Oracle ``SDO_GEOM.VALIDATE_GEOMETRY_WITH_CONTEXT(poly, 0.05) = 'TRUE'``
|
||||
========================== ================================================================
|
||||
=================================== ================================================================
|
||||
Backend SQL Equivalent
|
||||
=================================== ================================================================
|
||||
MariaDB, MySQL, PostGIS, SpatiaLite ``ST_IsValid(poly)``
|
||||
Oracle ``SDO_GEOM.VALIDATE_GEOMETRY_WITH_CONTEXT(poly, 0.05) = 'TRUE'``
|
||||
=================================== ================================================================
|
||||
|
||||
.. versionchanged:: 5.2
|
||||
|
||||
MariaDB 11.7+ support was added.
|
||||
|
||||
.. fieldlookup:: overlaps
|
||||
|
||||
@ -870,8 +885,8 @@ Example:
|
||||
|
||||
.. class:: Collect(geo_field, filter=None)
|
||||
|
||||
*Availability*: `PostGIS <https://postgis.net/docs/ST_Collect.html>`__, MySQL,
|
||||
SpatiaLite
|
||||
*Availability*: `PostGIS <https://postgis.net/docs/ST_Collect.html>`__,
|
||||
MariaDB, MySQL, SpatiaLite
|
||||
|
||||
Returns a ``GEOMETRYCOLLECTION`` or a ``MULTI`` geometry object from the geometry
|
||||
column. This is analogous to a simplified version of the :class:`Union`
|
||||
@ -883,6 +898,10 @@ caring about dissolving boundaries.
|
||||
|
||||
MySQL 8.0.24+ support was added.
|
||||
|
||||
.. versionchanged:: 5.2
|
||||
|
||||
MariaDB 11.7+ support was added.
|
||||
|
||||
``Extent``
|
||||
~~~~~~~~~~
|
||||
|
||||
|
@ -11,7 +11,25 @@ Django provides convenient ways to access the default storage class:
|
||||
|
||||
.. data:: storages
|
||||
|
||||
Storage instances as defined by :setting:`STORAGES`.
|
||||
A dictionary-like object that allows retrieving a storage instance using
|
||||
its alias as defined by :setting:`STORAGES`.
|
||||
|
||||
``storages`` has an attribute ``backends``, which defaults to the raw value
|
||||
provided in :setting:`STORAGES`.
|
||||
|
||||
Additionally, ``storages`` provides a ``create_storage()`` method that
|
||||
accepts the dictionary used in :setting:`STORAGES` for a backend, and
|
||||
returns a storage instance based on that backend definition. This may be
|
||||
useful for third-party packages needing to instantiate storages in tests:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from django.core.files.storage import storages
|
||||
>>> storages.backends
|
||||
{'default': {'BACKEND': 'django.core.files.storage.FileSystemStorage'},
|
||||
'staticfiles': {'BACKEND': 'django.contrib.staticfiles.storage.StaticFilesStorage'},
|
||||
'custom': {'BACKEND': 'package.storage.CustomStorage'}}
|
||||
>>> storage_instance = storages.create_storage({"BACKEND": "package.storage.CustomStorage"})
|
||||
|
||||
.. class:: DefaultStorage
|
||||
|
||||
|
@ -406,8 +406,8 @@ process:
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> f.base_fields["subject"].label_suffix = "?"
|
||||
>>> another_f = CommentForm(auto_id=False)
|
||||
>>> f.as_div().split("</div>")[0]
|
||||
>>> another_f = ContactForm(auto_id=False)
|
||||
>>> another_f.as_div().split("</div>")[0]
|
||||
'<div><label for="id_subject">Subject?</label><input type="text" name="subject" maxlength="100" required id="id_subject">'
|
||||
|
||||
Accessing "clean" data
|
||||
@ -511,7 +511,7 @@ empty string, because ``nick_name`` is ``CharField``, and ``CharField``\s treat
|
||||
empty values as an empty string. Each field type knows what its "blank" value
|
||||
is -- e.g., for ``DateField``, it's ``None`` instead of the empty string. For
|
||||
full details on each field's behavior in this case, see the "Empty value" note
|
||||
for each field in the "Built-in ``Field`` classes" section below.
|
||||
for each field in the :ref:`built-in-fields` section below.
|
||||
|
||||
You can write code to perform validation for particular form fields (based on
|
||||
their name) or for the form as a whole (considering combinations of various
|
||||
@ -770,7 +770,7 @@ The template used by ``as_table()``. Default: ``'django/forms/table.html'``.
|
||||
>>> f = ContactForm()
|
||||
>>> f.as_table()
|
||||
'<tr><th><label for="id_subject">Subject:</label></th><td><input id="id_subject" type="text" name="subject" maxlength="100" required></td></tr>\n<tr><th><label for="id_message">Message:</label></th><td><input type="text" name="message" id="id_message" required></td></tr>\n<tr><th><label for="id_sender">Sender:</label></th><td><input type="email" name="sender" id="id_sender" required></td></tr>\n<tr><th><label for="id_cc_myself">Cc myself:</label></th><td><input type="checkbox" name="cc_myself" id="id_cc_myself"></td></tr>'
|
||||
>>> print(f)
|
||||
>>> print(f.as_table())
|
||||
<tr><th><label for="id_subject">Subject:</label></th><td><input id="id_subject" type="text" name="subject" maxlength="100" required></td></tr>
|
||||
<tr><th><label for="id_message">Message:</label></th><td><input type="text" name="message" id="id_message" required></td></tr>
|
||||
<tr><th><label for="id_sender">Sender:</label></th><td><input type="email" name="sender" id="id_sender" required></td></tr>
|
||||
|
@ -282,27 +282,47 @@ PostgreSQL 15+.
|
||||
|
||||
.. attribute:: UniqueConstraint.violation_error_code
|
||||
|
||||
The error code used when ``ValidationError`` is raised during
|
||||
:ref:`model validation <validating-objects>`. Defaults to ``None``.
|
||||
The error code used when a ``ValidationError`` is raised during
|
||||
:ref:`model validation <validating-objects>`.
|
||||
|
||||
This code is *not used* for :class:`UniqueConstraint`\s with
|
||||
:attr:`~UniqueConstraint.fields` and without a
|
||||
:attr:`~UniqueConstraint.condition`. Such :class:`~UniqueConstraint`\s have the
|
||||
same error code as constraints defined with :attr:`.Field.unique` or in
|
||||
:attr:`Meta.unique_together <django.db.models.Options.constraints>`.
|
||||
Defaults to :attr:`.BaseConstraint.violation_error_code`, when either
|
||||
:attr:`.UniqueConstraint.condition` is set or :attr:`.UniqueConstraint.fields`
|
||||
is not set.
|
||||
|
||||
If :attr:`.UniqueConstraint.fields` is set without a
|
||||
:attr:`.UniqueConstraint.condition`, defaults to the :attr:`Meta.unique_together
|
||||
<django.db.models.Options.unique_together>` error code when there are multiple
|
||||
fields, and to the :attr:`.Field.unique` error code when there is a single
|
||||
field.
|
||||
|
||||
.. versionchanged:: 5.2
|
||||
|
||||
In older versions, the provided
|
||||
:attr:`.UniqueConstraint.violation_error_code` was not used when
|
||||
:attr:`.UniqueConstraint.fields` was set without a
|
||||
:attr:`.UniqueConstraint.condition`.
|
||||
|
||||
``violation_error_message``
|
||||
---------------------------
|
||||
|
||||
.. attribute:: UniqueConstraint.violation_error_message
|
||||
|
||||
The error message used when ``ValidationError`` is raised during
|
||||
:ref:`model validation <validating-objects>`. Defaults to
|
||||
:attr:`.BaseConstraint.violation_error_message`.
|
||||
The error message used when a ``ValidationError`` is raised during
|
||||
:ref:`model validation <validating-objects>`.
|
||||
|
||||
This message is *not used* for :class:`UniqueConstraint`\s with
|
||||
:attr:`~UniqueConstraint.fields` and without a
|
||||
:attr:`~UniqueConstraint.condition`. Such :class:`~UniqueConstraint`\s show the
|
||||
same message as constraints defined with
|
||||
:attr:`.Field.unique` or in
|
||||
:attr:`Meta.unique_together <django.db.models.Options.constraints>`.
|
||||
Defaults to :attr:`.BaseConstraint.violation_error_message`, when either
|
||||
:attr:`.UniqueConstraint.condition` is set or :attr:`.UniqueConstraint.fields`
|
||||
is not set.
|
||||
|
||||
If :attr:`.UniqueConstraint.fields` is set without a
|
||||
:attr:`.UniqueConstraint.condition`, defaults to the :attr:`Meta.unique_together
|
||||
<django.db.models.Options.unique_together>` error message when there are
|
||||
multiple fields, and to the :attr:`.Field.unique` error message when there is a
|
||||
single field.
|
||||
|
||||
.. versionchanged:: 5.2
|
||||
|
||||
In older versions, the provided
|
||||
:attr:`.UniqueConstraint.violation_error_message` was not used when
|
||||
:attr:`.UniqueConstraint.fields` was set without a
|
||||
:attr:`.UniqueConstraint.condition`.
|
||||
|
@ -22,9 +22,9 @@ This document contains all the API references of :class:`Field` including the
|
||||
|
||||
.. note::
|
||||
|
||||
Technically, these models are defined in :mod:`django.db.models.fields`, but
|
||||
for convenience they're imported into :mod:`django.db.models`; the standard
|
||||
convention is to use ``from django.db import models`` and refer to fields as
|
||||
Fields are defined in :mod:`django.db.models.fields`, but for convenience
|
||||
they're imported into :mod:`django.db.models`. The standard convention is
|
||||
to use ``from django.db import models`` and refer to fields as
|
||||
``models.<Foo>Field``.
|
||||
|
||||
.. _common-model-field-options:
|
||||
@ -426,6 +426,11 @@ precedence when creating instances in Python code. ``db_default`` will still be
|
||||
set at the database level and will be used when inserting rows outside of the
|
||||
ORM or when adding a new field in a migration.
|
||||
|
||||
If a field has a ``db_default`` without a ``default`` set and no value is
|
||||
assigned to the field, a ``DatabaseDefault`` object is returned as the field
|
||||
value on unsaved model instances. The actual value for the field is determined
|
||||
by the database when the model instance is saved.
|
||||
|
||||
``db_index``
|
||||
------------
|
||||
|
||||
@ -1628,80 +1633,25 @@ Django also defines a set of fields that represent relations.
|
||||
.. class:: ForeignKey(to, on_delete, **options)
|
||||
|
||||
A many-to-one relationship. Requires two positional arguments: the class to
|
||||
which the model is related and the :attr:`~ForeignKey.on_delete` option.
|
||||
|
||||
.. _recursive-relationships:
|
||||
|
||||
To create a recursive relationship -- an object that has a many-to-one
|
||||
relationship with itself -- use ``models.ForeignKey('self',
|
||||
on_delete=models.CASCADE)``.
|
||||
|
||||
.. _lazy-relationships:
|
||||
|
||||
If you need to create a relationship on a model that has not yet been defined,
|
||||
you can use the name of the model, rather than the model object itself::
|
||||
which the model is related and the :attr:`~ForeignKey.on_delete` option::
|
||||
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Car(models.Model):
|
||||
manufacturer = models.ForeignKey(
|
||||
"Manufacturer",
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
# ...
|
||||
|
||||
|
||||
class Manufacturer(models.Model):
|
||||
# ...
|
||||
pass
|
||||
name = models.TextField()
|
||||
|
||||
Relationships defined this way on :ref:`abstract models
|
||||
<abstract-base-classes>` are resolved when the model is subclassed as a
|
||||
concrete model and are not relative to the abstract model's ``app_label``:
|
||||
|
||||
.. code-block:: python
|
||||
:caption: ``products/models.py``
|
||||
|
||||
from django.db import models
|
||||
|
||||
|
||||
class AbstractCar(models.Model):
|
||||
manufacturer = models.ForeignKey("Manufacturer", on_delete=models.CASCADE)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
.. code-block:: python
|
||||
:caption: ``production/models.py``
|
||||
|
||||
from django.db import models
|
||||
from products.models import AbstractCar
|
||||
|
||||
|
||||
class Manufacturer(models.Model):
|
||||
pass
|
||||
|
||||
|
||||
class Car(AbstractCar):
|
||||
pass
|
||||
|
||||
|
||||
# Car.manufacturer will point to `production.Manufacturer` here.
|
||||
|
||||
To refer to models defined in another application, you can explicitly specify
|
||||
a model with the full application label. For example, if the ``Manufacturer``
|
||||
model above is defined in another application called ``production``, you'd
|
||||
need to use::
|
||||
|
||||
class Car(models.Model):
|
||||
manufacturer = models.ForeignKey(
|
||||
"production.Manufacturer",
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
manufacturer = models.ForeignKey(Manufacturer, on_delete=models.CASCADE)
|
||||
|
||||
This sort of reference, called a lazy relationship, can be useful when
|
||||
resolving circular import dependencies between two applications.
|
||||
The first positional argument can be either a concrete model class or a
|
||||
:ref:`lazy reference <lazy-relationships>` to a model class.
|
||||
:ref:`Recursive relationships <recursive-relationships>`, where a model has a
|
||||
relationship with itself, are also supported.
|
||||
|
||||
See :attr:`ForeignKey.on_delete` for details on the second positional
|
||||
argument.
|
||||
|
||||
A database index is automatically created on the ``ForeignKey``. You can
|
||||
disable this by setting :attr:`~Field.db_index` to ``False``. You may want to
|
||||
@ -1714,9 +1664,9 @@ Database Representation
|
||||
|
||||
Behind the scenes, Django appends ``"_id"`` to the field name to create its
|
||||
database column name. In the above example, the database table for the ``Car``
|
||||
model will have a ``manufacturer_id`` column. (You can change this explicitly by
|
||||
specifying :attr:`~Field.db_column`) However, your code should never have to
|
||||
deal with the database column name, unless you write custom SQL. You'll always
|
||||
model will have a ``manufacturer_id`` column. You can change this explicitly by
|
||||
specifying :attr:`~Field.db_column`, however, your code should never have to
|
||||
deal with the database column name (unless you write custom SQL). You'll always
|
||||
deal with the field names of your model object.
|
||||
|
||||
.. _foreign-key-arguments:
|
||||
@ -2266,6 +2216,120 @@ accepted by :class:`ForeignKey`, plus one extra argument:
|
||||
See :doc:`One-to-one relationships </topics/db/examples/one_to_one>` for usage
|
||||
examples of ``OneToOneField``.
|
||||
|
||||
.. _lazy-relationships:
|
||||
|
||||
Lazy relationships
|
||||
------------------
|
||||
|
||||
Lazy relationships allow referencing models by their names (as strings) or
|
||||
creating recursive relationships. Strings can be used as the first argument in
|
||||
any relationship field to reference models lazily. A lazy reference can be
|
||||
either :ref:`recursive <recursive-relationships>`,
|
||||
:ref:`relative <relative-relationships>` or
|
||||
:ref:`absolute <absolute-relationships>`.
|
||||
|
||||
.. _recursive-relationships:
|
||||
|
||||
Recursive
|
||||
~~~~~~~~~
|
||||
|
||||
To define a relationship where a model references itself, use ``"self"`` as the
|
||||
first argument of the relationship field::
|
||||
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Manufacturer(models.Model):
|
||||
name = models.TextField()
|
||||
suppliers = models.ManyToManyField("self", symmetrical=False)
|
||||
|
||||
|
||||
When used in an :ref:`abstract model <abstract-base-classes>`, the recursive
|
||||
relationship resolves such that each concrete subclass references itself.
|
||||
|
||||
.. _relative-relationships:
|
||||
|
||||
Relative
|
||||
~~~~~~~~
|
||||
|
||||
When a relationship needs to be created with a model that has not been defined
|
||||
yet, it can be referenced by its name rather than the model object itself::
|
||||
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Car(models.Model):
|
||||
manufacturer = models.ForeignKey(
|
||||
"Manufacturer",
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
|
||||
|
||||
class Manufacturer(models.Model):
|
||||
name = models.TextField()
|
||||
suppliers = models.ManyToManyField("self", symmetrical=False)
|
||||
|
||||
Relationships defined this way on :ref:`abstract models
|
||||
<abstract-base-classes>` are resolved when the model is subclassed as a
|
||||
concrete model and are not relative to the abstract model's ``app_label``:
|
||||
|
||||
.. code-block:: python
|
||||
:caption: ``products/models.py``
|
||||
|
||||
from django.db import models
|
||||
|
||||
|
||||
class AbstractCar(models.Model):
|
||||
manufacturer = models.ForeignKey("Manufacturer", on_delete=models.CASCADE)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
.. code-block:: python
|
||||
:caption: ``production/models.py``
|
||||
|
||||
from django.db import models
|
||||
from products.models import AbstractCar
|
||||
|
||||
|
||||
class Manufacturer(models.Model):
|
||||
name = models.TextField()
|
||||
|
||||
|
||||
class Car(AbstractCar):
|
||||
pass
|
||||
|
||||
In this example, the ``Car.manufacturer`` relationship will resolve to
|
||||
``production.Manufacturer``, as it points to the concrete model defined
|
||||
within the ``production/models.py`` file.
|
||||
|
||||
.. admonition:: Reusable models with relative references
|
||||
|
||||
Relative references allow the creation of reusable abstract models with
|
||||
relationships that can resolve to different implementations of the
|
||||
referenced models in various subclasses across different applications.
|
||||
|
||||
.. _absolute-relationships:
|
||||
|
||||
Absolute
|
||||
~~~~~~~~
|
||||
|
||||
Absolute references specify a model using its ``app_label`` and class name,
|
||||
allowing for model references across different applications. This type of lazy
|
||||
relationship can also help resolve circular imports.
|
||||
|
||||
For example, if the ``Manufacturer`` model is defined in another application
|
||||
called ``thirdpartyapp``, it can be referenced as::
|
||||
|
||||
class Car(models.Model):
|
||||
manufacturer = models.ForeignKey(
|
||||
"thirdpartyapp.Manufacturer",
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
|
||||
Absolute references always point to the same model, even when used in an
|
||||
:ref:`abstract model <abstract-base-classes>`.
|
||||
|
||||
Field API reference
|
||||
===================
|
||||
|
||||
|
@ -3110,6 +3110,11 @@ there are triggers or if a function is called, even for a ``SELECT`` query.
|
||||
|
||||
Support for the ``generic_plan`` option on PostgreSQL 16+ was added.
|
||||
|
||||
.. versionchanged:: 5.2
|
||||
|
||||
Support for the ``memory`` and ``serialize`` options on PostgreSQL 17+ was
|
||||
added.
|
||||
|
||||
.. _field-lookups:
|
||||
|
||||
``Field`` lookups
|
||||
|
@ -833,6 +833,13 @@ Attributes
|
||||
|
||||
A bytestring representing the content, encoded from a string if necessary.
|
||||
|
||||
.. attribute:: HttpResponse.text
|
||||
|
||||
.. versionadded:: 5.2
|
||||
|
||||
A string representation of :attr:`HttpResponse.content`, decoded using the
|
||||
response's :attr:`HttpResponse.charset` (defaulting to ``UTF-8`` if empty).
|
||||
|
||||
.. attribute:: HttpResponse.cookies
|
||||
|
||||
A :py:obj:`http.cookies.SimpleCookie` object holding the cookies included
|
||||
@ -1272,6 +1279,9 @@ with the following notable differences:
|
||||
:attr:`~StreamingHttpResponse.streaming_content` attribute. This can be used
|
||||
in middleware to wrap the response iterable, but should not be consumed.
|
||||
|
||||
* It has no ``text`` attribute, as it would require iterating the response
|
||||
object.
|
||||
|
||||
* You cannot use the file-like object ``tell()`` or ``write()`` methods.
|
||||
Doing so will raise an exception.
|
||||
|
||||
|
@ -3100,7 +3100,7 @@ slightly different call:
|
||||
|
||||
{% load static %}
|
||||
{% static "images/hi.jpg" as myphoto %}
|
||||
<img src="{{ myphoto }}">
|
||||
<img src="{{ myphoto }}" alt="Hi!">
|
||||
|
||||
.. admonition:: Using Jinja2 templates?
|
||||
|
||||
|
@ -13,7 +13,8 @@ your code, Django provides the following function:
|
||||
.. function:: reverse(viewname, urlconf=None, args=None, kwargs=None, current_app=None)
|
||||
|
||||
``viewname`` can be a :ref:`URL pattern name <naming-url-patterns>` or the
|
||||
callable view object. For example, given the following ``url``::
|
||||
callable view object used in the URLconf. For example, given the following
|
||||
``url``::
|
||||
|
||||
from news import views
|
||||
|
||||
@ -79,6 +80,26 @@ use for reversing. By default, the root URLconf for the current thread is used.
|
||||
Applying further encoding (such as :func:`urllib.parse.quote`) to the output
|
||||
of ``reverse()`` may produce undesirable results.
|
||||
|
||||
.. admonition:: Reversing class-based views by view object
|
||||
|
||||
The view object can also be the result of calling
|
||||
:meth:`~django.views.generic.base.View.as_view` if the same view object is
|
||||
used in the URLConf. Following the original example, the view object could
|
||||
be defined as:
|
||||
|
||||
.. code-block:: python
|
||||
:caption: ``news/views.py``
|
||||
|
||||
from django.views import View
|
||||
|
||||
|
||||
class ArchiveView(View): ...
|
||||
|
||||
|
||||
archive = ArchiveView.as_view()
|
||||
|
||||
However, remember that namespaced views cannot be reversed by view object.
|
||||
|
||||
``reverse_lazy()``
|
||||
==================
|
||||
|
||||
|
@ -10,4 +10,13 @@ Django 5.1.3 fixes several bugs in 5.1.2 and adds compatibility with Python
|
||||
Bugfixes
|
||||
========
|
||||
|
||||
* ...
|
||||
* Fixed a bug in Django 5.1 where
|
||||
:class:`~django.core.validators.DomainNameValidator` accepted any input value
|
||||
that contained a valid domain name, rather than only input values that were a
|
||||
valid domain name (:ticket:`35845`).
|
||||
|
||||
* Fixed a regression in Django 5.1 that prevented the use of DB-IP databases
|
||||
with :class:`~django.contrib.gis.geoip2.GeoIP2` (:ticket:`35841`).
|
||||
|
||||
* Fixed a regression in Django 5.1 where non-ASCII fieldset names were not
|
||||
displayed when rendering admin fieldsets (:ticket:`35876`).
|
||||
|
@ -52,29 +52,29 @@ Minor features
|
||||
* The default iteration count for the PBKDF2 password hasher is increased from
|
||||
870,000 to 1,000,000.
|
||||
|
||||
* The following new asynchronous methods on are now provided, using an ``a``
|
||||
* The following new asynchronous methods are now provided, using an ``a``
|
||||
prefix:
|
||||
|
||||
* :meth:`.UserManager.acreate_user`
|
||||
* :meth:`.UserManager.acreate_superuser`
|
||||
* :meth:`.BaseUserManager.aget_by_natural_key`
|
||||
* :meth:`.User.aget_user_permissions()`
|
||||
* :meth:`.User.aget_all_permissions()`
|
||||
* :meth:`.User.aget_group_permissions()`
|
||||
* :meth:`.User.ahas_perm()`
|
||||
* :meth:`.User.ahas_perms()`
|
||||
* :meth:`.User.ahas_module_perms()`
|
||||
* :meth:`.User.aget_user_permissions()`
|
||||
* :meth:`.User.aget_group_permissions()`
|
||||
* :meth:`.User.ahas_perm()`
|
||||
* :meth:`.ModelBackend.aauthenticate()`
|
||||
* :meth:`.ModelBackend.aget_user_permissions()`
|
||||
* :meth:`.ModelBackend.aget_group_permissions()`
|
||||
* :meth:`.ModelBackend.aget_all_permissions()`
|
||||
* :meth:`.ModelBackend.ahas_perm()`
|
||||
* :meth:`.ModelBackend.ahas_module_perms()`
|
||||
* :meth:`.RemoteUserBackend.aauthenticate()`
|
||||
* :meth:`.RemoteUserBackend.aconfigure_user()`
|
||||
* :meth:`.User.aget_user_permissions`
|
||||
* :meth:`.User.aget_all_permissions`
|
||||
* :meth:`.User.aget_group_permissions`
|
||||
* :meth:`.User.ahas_perm`
|
||||
* :meth:`.User.ahas_perms`
|
||||
* :meth:`.User.ahas_module_perms`
|
||||
* :meth:`.User.aget_user_permissions`
|
||||
* :meth:`.User.aget_group_permissions`
|
||||
* :meth:`.User.ahas_perm`
|
||||
* :meth:`.ModelBackend.aauthenticate`
|
||||
* :meth:`.ModelBackend.aget_user_permissions`
|
||||
* :meth:`.ModelBackend.aget_group_permissions`
|
||||
* :meth:`.ModelBackend.aget_all_permissions`
|
||||
* :meth:`.ModelBackend.ahas_perm`
|
||||
* :meth:`.ModelBackend.ahas_module_perms`
|
||||
* :meth:`.RemoteUserBackend.aauthenticate`
|
||||
* :meth:`.RemoteUserBackend.aconfigure_user`
|
||||
|
||||
* Auth backends can now provide async implementations which are used when
|
||||
calling async auth functions (e.g.
|
||||
@ -82,6 +82,10 @@ Minor features
|
||||
improves performance. See :ref:`adding an async interface
|
||||
<writing-authentication-backends-async-interface>` for more details.
|
||||
|
||||
* The :ref:`password validator classes <included-password-validators>`
|
||||
now have a new method ``get_error_message()``, which can be overridden in
|
||||
subclasses to customize the error messages.
|
||||
|
||||
:mod:`django.contrib.contenttypes`
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@ -90,7 +94,19 @@ Minor features
|
||||
:mod:`django.contrib.gis`
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
* ...
|
||||
* GDAL now supports curved geometries ``CurvePolygon``, ``CompoundCurve``,
|
||||
``CircularString``, ``MultiSurface``, and ``MultiCurve`` via the new
|
||||
:attr:`.OGRGeometry.has_curve` property, and the
|
||||
:meth:`.OGRGeometry.get_linear_geometry` and
|
||||
:meth:`.OGRGeometry.get_curve_geometry` methods.
|
||||
|
||||
* :lookup:`coveredby` and :lookup:`covers` lookup are now supported on MySQL.
|
||||
|
||||
* :lookup:`coveredby` and :lookup:`isvalid` lookups,
|
||||
:class:`~django.contrib.gis.db.models.Collect` aggregation, and
|
||||
:class:`~django.contrib.gis.db.models.functions.GeoHash` and
|
||||
:class:`~django.contrib.gis.db.models.functions.IsValid` database functions
|
||||
are now supported on MariaDB 11.7+.
|
||||
|
||||
:mod:`django.contrib.messages`
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
@ -230,6 +246,10 @@ Management Commands
|
||||
setting the :envvar:`HIDE_PRODUCTION_WARNING` environment variable to
|
||||
``"true"``.
|
||||
|
||||
* The :djadmin:`makemigrations` and :djadmin:`migrate` commands have a new
|
||||
``Command.autodetector`` attribute for subclasses to override in order to use
|
||||
a custom autodetector class.
|
||||
|
||||
Migrations
|
||||
~~~~~~~~~~
|
||||
|
||||
@ -257,9 +277,15 @@ Models
|
||||
longer required to be set on SQLite, which supports unlimited ``VARCHAR``
|
||||
columns.
|
||||
|
||||
* :meth:`.QuerySet.explain` now supports the ``memory`` and ``serialize``
|
||||
options on PostgreSQL 17+.
|
||||
|
||||
Requests and Responses
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
* The new :attr:`.HttpResponse.text` property provides the string representation
|
||||
of :attr:`.HttpResponse.content`.
|
||||
|
||||
* The new :meth:`.HttpRequest.get_preferred_type` method can be used to query
|
||||
the preferred media type the client accepts.
|
||||
|
||||
@ -358,6 +384,11 @@ Miscellaneous
|
||||
* ``HttpRequest.accepted_types`` is now sorted by the client's preference, based
|
||||
on the request's ``Accept`` header.
|
||||
|
||||
* :attr:`.UniqueConstraint.violation_error_code` and
|
||||
:attr:`.UniqueConstraint.violation_error_message` are now always used when
|
||||
provided. Previously, these were ignored when :attr:`.UniqueConstraint.fields`
|
||||
were set without a :attr:`.UniqueConstraint.condition`.
|
||||
|
||||
* The :func:`~django.template.context_processors.debug` context processor is no
|
||||
longer included in the default project template.
|
||||
|
||||
|
@ -123,6 +123,7 @@ deduplicates
|
||||
deduplication
|
||||
deepcopy
|
||||
deferrable
|
||||
DEP
|
||||
deprecations
|
||||
deserialization
|
||||
deserialize
|
||||
|
@ -590,6 +590,8 @@ has no settings.
|
||||
The help texts and any errors from password validators are always returned in
|
||||
the order they are listed in :setting:`AUTH_PASSWORD_VALIDATORS`.
|
||||
|
||||
.. _included-password-validators:
|
||||
|
||||
Included validators
|
||||
-------------------
|
||||
|
||||
@ -600,6 +602,19 @@ Django includes four validators:
|
||||
Validates that the password is of a minimum length.
|
||||
The minimum length can be customized with the ``min_length`` parameter.
|
||||
|
||||
.. method:: get_error_message()
|
||||
|
||||
.. versionadded:: 5.2
|
||||
|
||||
A hook for customizing the ``ValidationError`` error message. Defaults
|
||||
to ``"This password is too short. It must contain at least <min_length>
|
||||
characters."``.
|
||||
|
||||
.. method:: get_help_text()
|
||||
|
||||
A hook for customizing the validator's help text. Defaults to ``"Your
|
||||
password must contain at least <min_length> characters."``.
|
||||
|
||||
.. class:: UserAttributeSimilarityValidator(user_attributes=DEFAULT_USER_ATTRIBUTES, max_similarity=0.7)
|
||||
|
||||
Validates that the password is sufficiently different from certain
|
||||
@ -617,6 +632,18 @@ Django includes four validators:
|
||||
``user_attributes``, whereas a value of 1.0 rejects only passwords that are
|
||||
identical to an attribute's value.
|
||||
|
||||
.. method:: get_error_message()
|
||||
|
||||
.. versionadded:: 5.2
|
||||
|
||||
A hook for customizing the ``ValidationError`` error message. Defaults
|
||||
to ``"The password is too similar to the <user_attribute>."``.
|
||||
|
||||
.. method:: get_help_text()
|
||||
|
||||
A hook for customizing the validator's help text. Defaults to ``"Your
|
||||
password can’t be too similar to your other personal information."``.
|
||||
|
||||
.. class:: CommonPasswordValidator(password_list_path=DEFAULT_PASSWORD_LIST_PATH)
|
||||
|
||||
Validates that the password is not a common password. This converts the
|
||||
@ -628,10 +655,34 @@ Django includes four validators:
|
||||
common passwords. This file should contain one lowercase password per line
|
||||
and may be plain text or gzipped.
|
||||
|
||||
.. method:: get_error_message()
|
||||
|
||||
.. versionadded:: 5.2
|
||||
|
||||
A hook for customizing the ``ValidationError`` error message. Defaults
|
||||
to ``"This password is too common."``.
|
||||
|
||||
.. method:: get_help_text()
|
||||
|
||||
A hook for customizing the validator's help text. Defaults to ``"Your
|
||||
password can’t be a commonly used password."``.
|
||||
|
||||
.. class:: NumericPasswordValidator()
|
||||
|
||||
Validate that the password is not entirely numeric.
|
||||
|
||||
.. method:: get_error_message()
|
||||
|
||||
.. versionadded:: 5.2
|
||||
|
||||
A hook for customizing the ``ValidationError`` error message. Defaults
|
||||
to ``"This password is entirely numeric."``.
|
||||
|
||||
.. method:: get_help_text()
|
||||
|
||||
A hook for customizing the validator's help text. Defaults to ``"Your
|
||||
password can’t be entirely numeric."``.
|
||||
|
||||
Integrating validation
|
||||
----------------------
|
||||
|
||||
|
@ -224,6 +224,15 @@ ones:
|
||||
object. If callable it will be called every time a new object is
|
||||
created.
|
||||
|
||||
:attr:`~Field.db_default`
|
||||
The database-computed default value for the field. This can be a literal
|
||||
value or a database function.
|
||||
|
||||
If both ``db_default`` and :attr:`Field.default` are set, ``default`` will
|
||||
take precedence when creating instances in Python code. ``db_default`` will
|
||||
still be set at the database level and will be used when inserting rows
|
||||
outside of the ORM or when adding a new field in a migration.
|
||||
|
||||
:attr:`~Field.help_text`
|
||||
Extra "help" text to be displayed with the form widget. It's useful for
|
||||
documentation even if your field isn't used on a form.
|
||||
|
@ -311,9 +311,11 @@ All parameters are optional and can be set at any time prior to calling the
|
||||
* ``bcc``: A list or tuple of addresses used in the "Bcc" header when
|
||||
sending the email.
|
||||
|
||||
* ``connection``: An email backend instance. Use this parameter if
|
||||
you want to use the same connection for multiple messages. If omitted, a
|
||||
new connection is created when ``send()`` is called.
|
||||
* ``connection``: An :ref:`email backend <topic-email-backends>` instance. Use
|
||||
this parameter if you are sending the ``EmailMessage`` via ``send()`` and you
|
||||
want to use the same connection for multiple messages. If omitted, a new
|
||||
connection is created when ``send()`` is called. This parameter is ignored
|
||||
when using :ref:`send_messages() <topics-sending-multiple-emails>`.
|
||||
|
||||
* ``attachments``: A list of attachments to put on the message. These can
|
||||
be instances of :class:`~email.mime.base.MIMEBase` or
|
||||
@ -728,9 +730,10 @@ destroying a connection every time you want to send an email.
|
||||
|
||||
There are two ways you tell an email backend to reuse a connection.
|
||||
|
||||
Firstly, you can use the ``send_messages()`` method. ``send_messages()`` takes
|
||||
a list of :class:`~django.core.mail.EmailMessage` instances (or subclasses),
|
||||
and sends them all using a single connection.
|
||||
Firstly, you can use the ``send_messages()`` method on a connection. This takes
|
||||
a list of :class:`EmailMessage` (or subclass) instances, and sends them all
|
||||
using that single connection. As a consequence, any :class:`connection
|
||||
<EmailMessage>` set on an individual message is ignored.
|
||||
|
||||
For example, if you have a function called ``get_notification_email()`` that
|
||||
returns a list of :class:`~django.core.mail.EmailMessage` objects representing
|
||||
|
@ -23,7 +23,7 @@ Here's a view that returns the current date and time, as an HTML document::
|
||||
|
||||
def current_datetime(request):
|
||||
now = datetime.datetime.now()
|
||||
html = "<html><body>It is now %s.</body></html>" % now
|
||||
html = '<html lang="en"><body>It is now %s.</body></html>' % now
|
||||
return HttpResponse(html)
|
||||
|
||||
Let's step through this code one line at a time:
|
||||
@ -225,7 +225,7 @@ Here's an example of an async view::
|
||||
|
||||
async def current_datetime(request):
|
||||
now = datetime.datetime.now()
|
||||
html = "<html><body>It is now %s.</body></html>" % now
|
||||
html = '<html lang="en"><body>It is now %s.</body></html>' % now
|
||||
return HttpResponse(html)
|
||||
|
||||
You can read more about Django's async support, and how to best use async
|
||||
|
@ -1801,7 +1801,7 @@ class TestInlineWithFieldsets(TestDataMixin, TestCase):
|
||||
# The second and third have the same "Advanced options" name, but the
|
||||
# second one has the "collapse" class.
|
||||
for x, classes in ((1, ""), (2, "collapse")):
|
||||
heading_id = f"fieldset-0-advanced-options-{x}-heading"
|
||||
heading_id = f"fieldset-0-{x}-heading"
|
||||
with self.subTest(heading_id=heading_id):
|
||||
self.assertContains(
|
||||
response,
|
||||
@ -1846,7 +1846,7 @@ class TestInlineWithFieldsets(TestDataMixin, TestCase):
|
||||
# Every fieldset defined for an inline's form.
|
||||
for z, fieldset in enumerate(inline_admin_form):
|
||||
if fieldset.name:
|
||||
heading_id = f"{prefix}-{y}-details-{z}-heading"
|
||||
heading_id = f"{prefix}-{y}-{z}-heading"
|
||||
self.assertContains(
|
||||
response,
|
||||
f'<fieldset class="module aligned {fieldset.classes}" '
|
||||
|
@ -2356,8 +2356,8 @@ class Discovery(SimpleTestCase):
|
||||
class CommandDBOptionChoiceTests(SimpleTestCase):
|
||||
def test_invalid_choice_db_option(self):
|
||||
expected_error = (
|
||||
"Error: argument --database: invalid choice: "
|
||||
"'deflaut' (choose from 'default', 'other')"
|
||||
r"Error: argument --database: invalid choice: 'deflaut' "
|
||||
r"\(choose from '?default'?, '?other'?\)"
|
||||
)
|
||||
args = [
|
||||
"changepassword",
|
||||
@ -2378,7 +2378,7 @@ class CommandDBOptionChoiceTests(SimpleTestCase):
|
||||
]
|
||||
|
||||
for arg in args:
|
||||
with self.assertRaisesMessage(CommandError, expected_error):
|
||||
with self.assertRaisesRegex(CommandError, expected_error):
|
||||
call_command(arg, "--database", "deflaut", verbosity=0)
|
||||
|
||||
|
||||
|
@ -237,6 +237,7 @@ class ArticleAdmin(ArticleAdminWithExtraUrl):
|
||||
"Some other fields",
|
||||
{"classes": ("wide",), "fields": ("date", "section", "sub_section")},
|
||||
),
|
||||
("이름", {"fields": ("another_section",)}),
|
||||
)
|
||||
|
||||
# These orderings aren't particularly useful but show that expressions can
|
||||
|
@ -102,7 +102,7 @@ class AutocompleteJsonViewTests(AdminViewBasicTestCase):
|
||||
request.user = self.superuser
|
||||
response = AutocompleteJsonView.as_view(**self.as_view_args)(request)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = json.loads(response.content.decode("utf-8"))
|
||||
data = json.loads(response.text)
|
||||
self.assertEqual(
|
||||
data,
|
||||
{
|
||||
@ -120,7 +120,7 @@ class AutocompleteJsonViewTests(AdminViewBasicTestCase):
|
||||
request.user = self.superuser
|
||||
response = AutocompleteJsonView.as_view(**self.as_view_args)(request)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = json.loads(response.content.decode("utf-8"))
|
||||
data = json.loads(response.text)
|
||||
self.assertEqual(
|
||||
data,
|
||||
{
|
||||
@ -150,7 +150,7 @@ class AutocompleteJsonViewTests(AdminViewBasicTestCase):
|
||||
request.user = self.superuser
|
||||
response = AutocompleteJsonView.as_view(**self.as_view_args)(request)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = json.loads(response.content.decode("utf-8"))
|
||||
data = json.loads(response.text)
|
||||
self.assertEqual(
|
||||
data,
|
||||
{
|
||||
@ -184,7 +184,7 @@ class AutocompleteJsonViewTests(AdminViewBasicTestCase):
|
||||
request.user = self.superuser
|
||||
response = AutocompleteJsonView.as_view(**self.as_view_args)(request)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = json.loads(response.content.decode("utf-8"))
|
||||
data = json.loads(response.text)
|
||||
self.assertEqual(
|
||||
data,
|
||||
{
|
||||
@ -205,7 +205,7 @@ class AutocompleteJsonViewTests(AdminViewBasicTestCase):
|
||||
request.user = self.superuser
|
||||
response = AutocompleteJsonView.as_view(**self.as_view_args)(request)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = json.loads(response.content.decode("utf-8"))
|
||||
data = json.loads(response.text)
|
||||
self.assertEqual(
|
||||
data,
|
||||
{
|
||||
@ -250,7 +250,7 @@ class AutocompleteJsonViewTests(AdminViewBasicTestCase):
|
||||
request.user = self.superuser
|
||||
response = AutocompleteJsonView.as_view(**self.as_view_args)(request)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = json.loads(response.content.decode("utf-8"))
|
||||
data = json.loads(response.text)
|
||||
self.assertEqual(
|
||||
data,
|
||||
{
|
||||
@ -306,7 +306,7 @@ class AutocompleteJsonViewTests(AdminViewBasicTestCase):
|
||||
with model_admin(Question, DistinctQuestionAdmin):
|
||||
response = AutocompleteJsonView.as_view(**self.as_view_args)(request)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = json.loads(response.content.decode("utf-8"))
|
||||
data = json.loads(response.text)
|
||||
self.assertEqual(len(data["results"]), 3)
|
||||
|
||||
def test_missing_search_fields(self):
|
||||
@ -335,7 +335,7 @@ class AutocompleteJsonViewTests(AdminViewBasicTestCase):
|
||||
with model_admin(Question, PKOrderingQuestionAdmin):
|
||||
response = AutocompleteJsonView.as_view(**self.as_view_args)(request)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = json.loads(response.content.decode("utf-8"))
|
||||
data = json.loads(response.text)
|
||||
self.assertEqual(
|
||||
data,
|
||||
{
|
||||
@ -352,7 +352,7 @@ class AutocompleteJsonViewTests(AdminViewBasicTestCase):
|
||||
with model_admin(Question, PKOrderingQuestionAdmin):
|
||||
response = AutocompleteJsonView.as_view(**self.as_view_args)(request)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = json.loads(response.content.decode("utf-8"))
|
||||
data = json.loads(response.text)
|
||||
self.assertEqual(
|
||||
data,
|
||||
{
|
||||
@ -380,7 +380,7 @@ class AutocompleteJsonViewTests(AdminViewBasicTestCase):
|
||||
request
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = json.loads(response.content.decode("utf-8"))
|
||||
data = json.loads(response.text)
|
||||
self.assertEqual(
|
||||
data,
|
||||
{
|
||||
|
@ -296,9 +296,7 @@ class AdminViewBasicTestCase(TestCase):
|
||||
self.assertLess(
|
||||
response.content.index(text1.encode()),
|
||||
response.content.index(text2.encode()),
|
||||
(failing_msg or "")
|
||||
+ "\nResponse:\n"
|
||||
+ response.content.decode(response.charset),
|
||||
(failing_msg or "") + "\nResponse:\n" + response.text,
|
||||
)
|
||||
|
||||
|
||||
@ -2535,6 +2533,19 @@ class AdminViewPermissionsTest(TestCase):
|
||||
self.assertContains(
|
||||
response, '<input type="submit" value="Save and view" name="_continue">'
|
||||
)
|
||||
self.assertContains(
|
||||
response,
|
||||
'<h2 id="fieldset-0-0-heading" class="fieldset-heading">Some fields</h2>',
|
||||
)
|
||||
self.assertContains(
|
||||
response,
|
||||
'<h2 id="fieldset-0-1-heading" class="fieldset-heading">'
|
||||
"Some other fields</h2>",
|
||||
)
|
||||
self.assertContains(
|
||||
response,
|
||||
'<h2 id="fieldset-0-2-heading" class="fieldset-heading">이름</h2>',
|
||||
)
|
||||
post = self.client.post(
|
||||
reverse("admin:admin_views_article_add"), add_dict, follow=False
|
||||
)
|
||||
@ -3603,7 +3614,7 @@ class AdminViewDeletedObjectsTest(TestCase):
|
||||
response = self.client.get(
|
||||
reverse("admin:admin_views_villain_delete", args=(self.v1.pk,))
|
||||
)
|
||||
self.assertRegex(response.content.decode(), pattern)
|
||||
self.assertRegex(response.text, pattern)
|
||||
|
||||
def test_cyclic(self):
|
||||
"""
|
||||
@ -8266,7 +8277,7 @@ class AdminKeepChangeListFiltersTests(TestCase):
|
||||
# Check the `change_view` link has the correct querystring.
|
||||
detail_link = re.search(
|
||||
'<a href="(.*?)">{}</a>'.format(self.joepublicuser.username),
|
||||
response.content.decode(),
|
||||
response.text,
|
||||
)
|
||||
self.assertURLEqual(detail_link[1], self.get_change_url())
|
||||
|
||||
@ -8278,7 +8289,7 @@ class AdminKeepChangeListFiltersTests(TestCase):
|
||||
# Check the form action.
|
||||
form_action = re.search(
|
||||
'<form action="(.*?)" method="post" id="user_form" novalidate>',
|
||||
response.content.decode(),
|
||||
response.text,
|
||||
)
|
||||
self.assertURLEqual(
|
||||
form_action[1], "?%s" % self.get_preserved_filters_querystring()
|
||||
@ -8286,13 +8297,13 @@ class AdminKeepChangeListFiltersTests(TestCase):
|
||||
|
||||
# Check the history link.
|
||||
history_link = re.search(
|
||||
'<a href="(.*?)" class="historylink">History</a>', response.content.decode()
|
||||
'<a href="(.*?)" class="historylink">History</a>', response.text
|
||||
)
|
||||
self.assertURLEqual(history_link[1], self.get_history_url())
|
||||
|
||||
# Check the delete link.
|
||||
delete_link = re.search(
|
||||
'<a href="(.*?)" class="deletelink">Delete</a>', response.content.decode()
|
||||
'<a href="(.*?)" class="deletelink">Delete</a>', response.text
|
||||
)
|
||||
self.assertURLEqual(delete_link[1], self.get_delete_url())
|
||||
|
||||
@ -8332,7 +8343,7 @@ class AdminKeepChangeListFiltersTests(TestCase):
|
||||
self.client.force_login(viewuser)
|
||||
response = self.client.get(self.get_change_url())
|
||||
close_link = re.search(
|
||||
'<a href="(.*?)" class="closelink">Close</a>', response.content.decode()
|
||||
'<a href="(.*?)" class="closelink">Close</a>', response.text
|
||||
)
|
||||
close_link = close_link[1].replace("&", "&")
|
||||
self.assertURLEqual(close_link, self.get_changelist_url())
|
||||
@ -8350,7 +8361,7 @@ class AdminKeepChangeListFiltersTests(TestCase):
|
||||
# Check the form action.
|
||||
form_action = re.search(
|
||||
'<form action="(.*?)" method="post" id="user_form" novalidate>',
|
||||
response.content.decode(),
|
||||
response.text,
|
||||
)
|
||||
self.assertURLEqual(
|
||||
form_action[1], "?%s" % self.get_preserved_filters_querystring()
|
||||
|
@ -1,4 +1,4 @@
|
||||
from asyncio import iscoroutinefunction
|
||||
from asgiref.sync import iscoroutinefunction
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import models
|
||||
|
@ -144,6 +144,20 @@ class MinimumLengthValidatorTest(SimpleTestCase):
|
||||
"Your password must contain at least 8 characters.",
|
||||
)
|
||||
|
||||
def test_custom_error(self):
|
||||
class CustomMinimumLengthValidator(MinimumLengthValidator):
|
||||
def get_error_message(self):
|
||||
return "Your password must be %d characters long" % self.min_length
|
||||
|
||||
expected_error = "Your password must be %d characters long"
|
||||
|
||||
with self.assertRaisesMessage(ValidationError, expected_error % 8) as cm:
|
||||
CustomMinimumLengthValidator().validate("1234567")
|
||||
self.assertEqual(cm.exception.error_list[0].code, "password_too_short")
|
||||
|
||||
with self.assertRaisesMessage(ValidationError, expected_error % 3) as cm:
|
||||
CustomMinimumLengthValidator(min_length=3).validate("12")
|
||||
|
||||
|
||||
class UserAttributeSimilarityValidatorTest(TestCase):
|
||||
def test_validate(self):
|
||||
@ -213,6 +227,42 @@ class UserAttributeSimilarityValidatorTest(TestCase):
|
||||
"Your password can’t be too similar to your other personal information.",
|
||||
)
|
||||
|
||||
def test_custom_error(self):
|
||||
class CustomUserAttributeSimilarityValidator(UserAttributeSimilarityValidator):
|
||||
def get_error_message(self):
|
||||
return "The password is too close to the %(verbose_name)s."
|
||||
|
||||
user = User.objects.create_user(
|
||||
username="testclient",
|
||||
password="password",
|
||||
email="testclient@example.com",
|
||||
first_name="Test",
|
||||
last_name="Client",
|
||||
)
|
||||
|
||||
expected_error = "The password is too close to the %s."
|
||||
|
||||
with self.assertRaisesMessage(ValidationError, expected_error % "username"):
|
||||
CustomUserAttributeSimilarityValidator().validate("testclient", user=user)
|
||||
|
||||
def test_custom_error_verbose_name_not_used(self):
|
||||
class CustomUserAttributeSimilarityValidator(UserAttributeSimilarityValidator):
|
||||
def get_error_message(self):
|
||||
return "The password is too close to a user attribute."
|
||||
|
||||
user = User.objects.create_user(
|
||||
username="testclient",
|
||||
password="password",
|
||||
email="testclient@example.com",
|
||||
first_name="Test",
|
||||
last_name="Client",
|
||||
)
|
||||
|
||||
expected_error = "The password is too close to a user attribute."
|
||||
|
||||
with self.assertRaisesMessage(ValidationError, expected_error):
|
||||
CustomUserAttributeSimilarityValidator().validate("testclient", user=user)
|
||||
|
||||
|
||||
class CommonPasswordValidatorTest(SimpleTestCase):
|
||||
def test_validate(self):
|
||||
@ -247,6 +297,16 @@ class CommonPasswordValidatorTest(SimpleTestCase):
|
||||
"Your password can’t be a commonly used password.",
|
||||
)
|
||||
|
||||
def test_custom_error(self):
|
||||
class CustomCommonPasswordValidator(CommonPasswordValidator):
|
||||
def get_error_message(self):
|
||||
return "This password has been used too much."
|
||||
|
||||
expected_error = "This password has been used too much."
|
||||
|
||||
with self.assertRaisesMessage(ValidationError, expected_error):
|
||||
CustomCommonPasswordValidator().validate("godzilla")
|
||||
|
||||
|
||||
class NumericPasswordValidatorTest(SimpleTestCase):
|
||||
def test_validate(self):
|
||||
@ -264,6 +324,16 @@ class NumericPasswordValidatorTest(SimpleTestCase):
|
||||
"Your password can’t be entirely numeric.",
|
||||
)
|
||||
|
||||
def test_custom_error(self):
|
||||
class CustomNumericPasswordValidator(NumericPasswordValidator):
|
||||
def get_error_message(self):
|
||||
return "This password is all digits."
|
||||
|
||||
expected_error = "This password is all digits."
|
||||
|
||||
with self.assertRaisesMessage(ValidationError, expected_error):
|
||||
CustomNumericPasswordValidator().validate("42424242")
|
||||
|
||||
|
||||
class UsernameValidatorsTests(SimpleTestCase):
|
||||
def test_unicode_validator(self):
|
||||
|
@ -1521,7 +1521,7 @@ class ChangelistTests(MessagesTestMixin, AuthViewsTestCase):
|
||||
# Test the link inside password field help_text.
|
||||
rel_link = re.search(
|
||||
r'<a class="button" href="([^"]*)">Reset password</a>',
|
||||
response.content.decode(),
|
||||
response.text,
|
||||
)[1]
|
||||
self.assertEqual(urljoin(user_change_url, rel_link), password_change_url)
|
||||
|
||||
@ -1617,7 +1617,7 @@ class ChangelistTests(MessagesTestMixin, AuthViewsTestCase):
|
||||
# Test the link inside password field help_text.
|
||||
rel_link = re.search(
|
||||
r'<a class="button" href="([^"]*)">Set password</a>',
|
||||
response.content.decode(),
|
||||
response.text,
|
||||
)[1]
|
||||
self.assertEqual(urljoin(user_change_url, rel_link), password_change_url)
|
||||
|
||||
|
@ -0,0 +1,7 @@
|
||||
from django.core.management.commands.makemigrations import (
|
||||
Command as MakeMigrationsCommand,
|
||||
)
|
||||
|
||||
|
||||
class Command(MakeMigrationsCommand):
|
||||
autodetector = int
|
25
tests/check_framework/test_commands.py
Normal file
@ -0,0 +1,25 @@
|
||||
from django.core import checks
|
||||
from django.core.checks import Error
|
||||
from django.test import SimpleTestCase
|
||||
from django.test.utils import isolate_apps, override_settings, override_system_checks
|
||||
|
||||
|
||||
@isolate_apps("check_framework.custom_commands_app", attr_name="apps")
|
||||
@override_settings(INSTALLED_APPS=["check_framework.custom_commands_app"])
|
||||
@override_system_checks([checks.commands.migrate_and_makemigrations_autodetector])
|
||||
class CommandCheckTests(SimpleTestCase):
|
||||
def test_migrate_and_makemigrations_autodetector_different(self):
|
||||
expected_error = Error(
|
||||
"The migrate and makemigrations commands must have the same "
|
||||
"autodetector.",
|
||||
hint=(
|
||||
"makemigrations.Command.autodetector is int, but "
|
||||
"migrate.Command.autodetector is MigrationAutodetector."
|
||||
),
|
||||
id="commands.E001",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
checks.run_checks(app_configs=self.apps.get_app_configs()),
|
||||
[expected_error],
|
||||
)
|
@ -72,15 +72,13 @@ class GeneratedFieldVirtualProduct(models.Model):
|
||||
class UniqueConstraintProduct(models.Model):
|
||||
name = models.CharField(max_length=255)
|
||||
color = models.CharField(max_length=32, null=True)
|
||||
age = models.IntegerField(null=True)
|
||||
|
||||
class Meta:
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["name", "color"],
|
||||
name="name_color_uniq",
|
||||
# Custom message and error code are ignored.
|
||||
violation_error_code="custom_code",
|
||||
violation_error_message="Custom message",
|
||||
)
|
||||
]
|
||||
|
||||
|
@ -953,6 +953,41 @@ class UniqueConstraintTests(TestCase):
|
||||
ChildUniqueConstraintProduct(name=self.p1.name, color=self.p1.color),
|
||||
)
|
||||
|
||||
def test_validate_unique_custom_code_and_message(self):
|
||||
product = UniqueConstraintProduct.objects.create(
|
||||
name="test", color="red", age=42
|
||||
)
|
||||
code = "custom_code"
|
||||
message = "Custom message"
|
||||
multiple_fields_constraint = models.UniqueConstraint(
|
||||
fields=["color", "age"],
|
||||
name="color_age_uniq",
|
||||
violation_error_code=code,
|
||||
violation_error_message=message,
|
||||
)
|
||||
single_field_constraint = models.UniqueConstraint(
|
||||
fields=["color"],
|
||||
name="color_uniq",
|
||||
violation_error_code=code,
|
||||
violation_error_message=message,
|
||||
)
|
||||
|
||||
with self.assertRaisesMessage(ValidationError, message) as cm:
|
||||
multiple_fields_constraint.validate(
|
||||
UniqueConstraintProduct,
|
||||
UniqueConstraintProduct(
|
||||
name="new-test", color=product.color, age=product.age
|
||||
),
|
||||
)
|
||||
self.assertEqual(cm.exception.code, code)
|
||||
|
||||
with self.assertRaisesMessage(ValidationError, message) as cm:
|
||||
single_field_constraint.validate(
|
||||
UniqueConstraintProduct,
|
||||
UniqueConstraintProduct(name="new-test", color=product.color),
|
||||
)
|
||||
self.assertEqual(cm.exception.code, code)
|
||||
|
||||
@skipUnlessDBFeature("supports_table_check_constraints")
|
||||
def test_validate_fields_unattached(self):
|
||||
Product.objects.create(price=42)
|
||||
|
@ -1481,9 +1481,11 @@ class CsrfInErrorHandlingViewsTests(CsrfFunctionTestMixin, SimpleTestCase):
|
||||
response = self.client.get("/does not exist/")
|
||||
# The error handler returns status code 599.
|
||||
self.assertEqual(response.status_code, 599)
|
||||
token1 = response.content.decode("ascii")
|
||||
response.charset = "ascii"
|
||||
token1 = response.text
|
||||
response = self.client.get("/does not exist/")
|
||||
self.assertEqual(response.status_code, 599)
|
||||
token2 = response.content.decode("ascii")
|
||||
response.charset = "ascii"
|
||||
token2 = response.text
|
||||
secret2 = _unmask_cipher_token(token2)
|
||||
self.assertMaskedSecretCorrect(token1, secret2)
|
||||
|
@ -1,3 +1,4 @@
|
||||
import itertools
|
||||
import unittest
|
||||
|
||||
from django.db import NotSupportedError, connection
|
||||
@ -129,6 +130,37 @@ class TupleLookupsTests(TestCase):
|
||||
(self.contact_1, self.contact_2, self.contact_5),
|
||||
)
|
||||
|
||||
def test_tuple_in_rhs_must_be_collection_of_tuples_or_lists(self):
|
||||
test_cases = (
|
||||
(1, 2, 3),
|
||||
((1, 2), (3, 4), None),
|
||||
)
|
||||
|
||||
for rhs in test_cases:
|
||||
with self.subTest(rhs=rhs):
|
||||
with self.assertRaisesMessage(
|
||||
ValueError,
|
||||
"'in' lookup of ('customer_code', 'company_code') "
|
||||
"must be a collection of tuples or lists",
|
||||
):
|
||||
TupleIn((F("customer_code"), F("company_code")), rhs)
|
||||
|
||||
def test_tuple_in_rhs_must_have_2_elements_each(self):
|
||||
test_cases = (
|
||||
((),),
|
||||
((1,),),
|
||||
((1, 2, 3),),
|
||||
)
|
||||
|
||||
for rhs in test_cases:
|
||||
with self.subTest(rhs=rhs):
|
||||
with self.assertRaisesMessage(
|
||||
ValueError,
|
||||
"'in' lookup of ('customer_code', 'company_code') "
|
||||
"must have 2 elements each",
|
||||
):
|
||||
TupleIn((F("customer_code"), F("company_code")), rhs)
|
||||
|
||||
def test_lt(self):
|
||||
c1, c2, c3, c4, c5, c6 = (
|
||||
self.contact_1,
|
||||
@ -358,8 +390,8 @@ class TupleLookupsTests(TestCase):
|
||||
)
|
||||
|
||||
def test_lookup_errors(self):
|
||||
m_2_elements = "'%s' lookup of 'customer' field must have 2 elements"
|
||||
m_2_elements_each = "'in' lookup of 'customer' field must have 2 elements each"
|
||||
m_2_elements = "'%s' lookup of 'customer' must have 2 elements"
|
||||
m_2_elements_each = "'in' lookup of 'customer' must have 2 elements each"
|
||||
test_cases = (
|
||||
({"customer": 1}, m_2_elements % "exact"),
|
||||
({"customer": (1, 2, 3)}, m_2_elements % "exact"),
|
||||
@ -381,3 +413,77 @@ class TupleLookupsTests(TestCase):
|
||||
self.assertRaisesMessage(ValueError, message),
|
||||
):
|
||||
Contact.objects.get(**kwargs)
|
||||
|
||||
def test_tuple_lookup_names(self):
|
||||
test_cases = (
|
||||
(TupleExact, "exact"),
|
||||
(TupleGreaterThan, "gt"),
|
||||
(TupleGreaterThanOrEqual, "gte"),
|
||||
(TupleLessThan, "lt"),
|
||||
(TupleLessThanOrEqual, "lte"),
|
||||
(TupleIn, "in"),
|
||||
(TupleIsNull, "isnull"),
|
||||
)
|
||||
|
||||
for lookup_class, lookup_name in test_cases:
|
||||
with self.subTest(lookup_name):
|
||||
self.assertEqual(lookup_class.lookup_name, lookup_name)
|
||||
|
||||
def test_tuple_lookup_rhs_must_be_tuple_or_list(self):
|
||||
test_cases = itertools.product(
|
||||
(
|
||||
TupleExact,
|
||||
TupleGreaterThan,
|
||||
TupleGreaterThanOrEqual,
|
||||
TupleLessThan,
|
||||
TupleLessThanOrEqual,
|
||||
TupleIn,
|
||||
),
|
||||
(
|
||||
0,
|
||||
1,
|
||||
None,
|
||||
True,
|
||||
False,
|
||||
{"foo": "bar"},
|
||||
),
|
||||
)
|
||||
|
||||
for lookup_cls, rhs in test_cases:
|
||||
lookup_name = lookup_cls.lookup_name
|
||||
with self.subTest(lookup_name=lookup_name, rhs=rhs):
|
||||
with self.assertRaisesMessage(
|
||||
ValueError,
|
||||
f"'{lookup_name}' lookup of ('customer_code', 'company_code') "
|
||||
"must be a tuple or a list",
|
||||
):
|
||||
lookup_cls((F("customer_code"), F("company_code")), rhs)
|
||||
|
||||
def test_tuple_lookup_rhs_must_have_2_elements(self):
|
||||
test_cases = itertools.product(
|
||||
(
|
||||
TupleExact,
|
||||
TupleGreaterThan,
|
||||
TupleGreaterThanOrEqual,
|
||||
TupleLessThan,
|
||||
TupleLessThanOrEqual,
|
||||
),
|
||||
(
|
||||
[],
|
||||
[1],
|
||||
[1, 2, 3],
|
||||
(),
|
||||
(1,),
|
||||
(1, 2, 3),
|
||||
),
|
||||
)
|
||||
|
||||
for lookup_cls, rhs in test_cases:
|
||||
lookup_name = lookup_cls.lookup_name
|
||||
with self.subTest(lookup_name=lookup_name, rhs=rhs):
|
||||
with self.assertRaisesMessage(
|
||||
ValueError,
|
||||
f"'{lookup_name}' lookup of ('customer_code', 'company_code') "
|
||||
"must have 2 elements",
|
||||
):
|
||||
lookup_cls((F("customer_code"), F("company_code")), rhs)
|
||||
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
@ -1,4 +0,0 @@
|
||||
This work is licensed under the Creative Commons Attribution-ShareAlike 3.0
|
||||
Unported License. To view a copy of this license, visit
|
||||
http://creativecommons.org/licenses/by-sa/3.0/ or send a letter to Creative
|
||||
Commons, 444 Castro Street, Suite 900, Mountain View, California, 94041, USA.
|
@ -1,3 +0,0 @@
|
||||
These test databases are taken from the following repository:
|
||||
|
||||
https://github.com/maxmind/MaxMind-DB/
|
28
tests/gis_tests/data/geoip2/README.md
Normal file
@ -0,0 +1,28 @@
|
||||
# GeoIP2 and GeoLite2 Test Databases
|
||||
|
||||
The following test databases are provided under [this license][0]:
|
||||
|
||||
- `GeoIP2-City-Test.mmdb`
|
||||
- `GeoIP2-Country-Test.mmdb`
|
||||
- `GeoLite2-ASN-Test.mmdb`
|
||||
- `GeoLite2-City-Test.mmdb`
|
||||
- `GeoLite2-Country-Test.mmdb`
|
||||
|
||||
Updates can be found in [this repository][1].
|
||||
|
||||
[0]: https://github.com/maxmind/MaxMind-DB/blob/main/LICENSE-MIT
|
||||
[1]: https://github.com/maxmind/MaxMind-DB/tree/main/test-data
|
||||
|
||||
# DB-IP Lite Test Databases
|
||||
|
||||
The following test databases are provided under [this license][2]:
|
||||
|
||||
- `dbip-city-lite-test.mmdb`
|
||||
- `dbip-country-lite-test.mmdb`
|
||||
|
||||
They have been modified to strip them down to a minimal dataset for testing.
|
||||
|
||||
Updates can be found at [this download page][3] from DB-IP.
|
||||
|
||||
[2]: https://creativecommons.org/licenses/by/4.0/
|
||||
[3]: https://db-ip.com/db/lite.php
|
BIN
tests/gis_tests/data/geoip2/dbip-city-lite-test.mmdb
Normal file
After Width: | Height: | Size: 1.4 KiB |