Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions django/db/models/fields/related.py
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,14 @@ def __copy__(self):
obj.__dict__.pop("reverse_path_infos", None)
return obj

@property
def non_db_attrs(self):
if isinstance(self.remote_field.on_delete, DatabaseOnDelete):
# Database-level on_delete options are part of the column
# definition.
return super().non_db_attrs
return super().non_db_attrs + ("on_delete",)

def check(self, **kwargs):
return [
*super().check(**kwargs),
Expand Down
4 changes: 3 additions & 1 deletion docs/ref/forms/renderers.txt
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ setting. It defaults to

By specifying a custom form renderer and overriding
:attr:`~.BaseRenderer.form_template_name` you can adjust the default form
markup across your project from a single place.
markup across your project from a single place. Note that this will include
forms in :doc:`the admin site </ref/contrib/admin/index>` and third-party
packages that use the default renderer.

You can also provide a custom renderer per-form or per-widget by setting the
:attr:`.Form.default_renderer` attribute or by using the ``renderer`` argument
Expand Down
6 changes: 5 additions & 1 deletion docs/ref/settings.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1785,14 +1785,18 @@ generate correct URLs when ``FORCE_SCRIPT_NAME`` is provided.

Default: ``'``:class:`django.forms.renderers.DjangoTemplates`\ ``'``

The class that renders forms and form widgets. It must implement
The default class that renders forms and form widgets. It must implement
:ref:`the low-level render API <low-level-widget-render-api>`. Included form
renderers are:

* ``'``:class:`django.forms.renderers.DjangoTemplates`\ ``'``
* ``'``:class:`django.forms.renderers.Jinja2`\ ``'``
* ``'``:class:`django.forms.renderers.TemplatesSetting`\ ``'``

Note that changing this setting affects all forms using the default renderer,
including those in :doc:`the admin site </ref/contrib/admin/index>` or
third-party packages.

.. setting:: FORMAT_MODULE_PATH

``FORMAT_MODULE_PATH``
Expand Down
5 changes: 5 additions & 0 deletions docs/releases/6.1.1.txt
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,8 @@ Bugfixes
created with :func:`~django.utils.safestring.mark_safe`, used as form media
assets were treated as asset paths rather than being rendered verbatim
(:ticket:`37262`).

* Fixed a regression in Django 6.1 that caused ``AlterField`` operations that
changed only the Python-level ``on_delete`` option of ``ForeignKey`` or
``OneToOneField`` fields to perform unnecessary schema changes
(:ticket:`37260`).
8 changes: 4 additions & 4 deletions tests/gis_tests/geoapp/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -808,25 +808,25 @@ def test_lookup_rejects_writing_or_fetching_rasters(self):
self.subTest(obj=obj),
self.assertRaisesMessage(DisallowedRasterLookup, msg),
):
City.objects.filter(point__contained=obj)
State.objects.filter(poly__intersects=obj)

# Strings having nothing to do with rasters raise a more generic error.
for obj in str(existing_path), "invalid":
msg = "String input unrecognized as WKT EWKT, and HEXEWKB."
with self.subTest(obj=obj), self.assertRaisesMessage(ValueError, msg):
City.objects.filter(point__contained=obj)
State.objects.filter(poly__intersects=obj)

def test_lookup_allows_writing_raster_from_bytes(self):
raster_path = Path(__file__).parent.parent / "data" / "rasters" / "raster.tif"
with open(raster_path, "rb") as raster_file:
raster_bytes = raster_file.read()
# Just get SQL to avoid gating on connection.supports_raster.
City.objects.filter(point__contained=raster_bytes).query
State.objects.filter(poly__intersects=raster_bytes).query

def test_lookup_allows_geos_geometry_string(self):
geojson = json.dumps({"type": "Point", "coordinates": [2, 49]})
# Just get SQL to avoid gating on connection.supports_raster.
City.objects.filter(point__contained=geojson).query
State.objects.filter(poly__intersects=geojson).query

@skipUnlessGISLookup("exact")
def test_lookup_against_nested_geometry_collection(self):
Expand Down
27 changes: 27 additions & 0 deletions tests/migrations/test_operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -2573,6 +2573,33 @@ def test_alter_field_add_db_column_noop(self):
operation.database_forwards(app_label, editor, new_state, project_state)
self.assertColumnExists(rider_table, "pony_id")

def test_alter_field_python_level_on_delete_noop(self):
"""
AlterField operation is a noop when changing only the Python-level
on_delete option.
"""
app_label = "test_alflodnoop"
project_state = self.set_up_test_model(app_label, related_model=True)
operation = migrations.AlterField(
"Rider", "pony", models.ForeignKey("Pony", models.PROTECT)
)
new_state = project_state.clone()
operation.state_forwards(app_label, new_state)
self.assertIs(
project_state.models[app_label, "rider"]
.fields["pony"]
.remote_field.on_delete,
models.CASCADE,
)
self.assertIs(
new_state.models[app_label, "rider"].fields["pony"].remote_field.on_delete,
models.PROTECT,
)
with connection.schema_editor() as editor, self.assertNumQueries(0):
operation.database_forwards(app_label, editor, project_state, new_state)
with connection.schema_editor() as editor, self.assertNumQueries(0):
operation.database_backwards(app_label, editor, new_state, project_state)

def test_alter_field_foreignobject_noop(self):
app_label = "test_alflfo_noop"
project_state = self.set_up_test_model(app_label)
Expand Down
49 changes: 48 additions & 1 deletion tests/schema/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -635,10 +635,57 @@ def test_fk_alter_on_delete(self):
):
editor.alter_field(Book, old_field, new_field)
self.assertForeignKeyExists(Book, "author_id", "schema_author")
self.assertGreater(len(ctx.captured_queries), 0)
self.assertIs(
any("ON DELETE" in query["sql"] for query in ctx.captured_queries), False
)

@skipUnlessDBFeature("supports_foreign_keys", "can_introspect_foreign_keys")
def test_fk_alter_on_delete_python_level_noop(self):
with connection.schema_editor() as editor:
editor.create_model(Author)
editor.create_model(Book)
old_field = Book._meta.get_field("author")
new_field = ForeignKey(Author, PROTECT)
new_field.set_attributes_from_name("author")
# Changing between Python-level on_delete options doesn't require
# database changes.
with connection.schema_editor() as editor, self.assertNumQueries(0):
editor.alter_field(Book, old_field, new_field, strict=True)
with connection.schema_editor() as editor, self.assertNumQueries(0):
editor.alter_field(Book, new_field, old_field, strict=True)

@isolate_apps("schema")
@skipUnlessDBFeature("supports_foreign_keys", "can_introspect_foreign_keys")
def test_fk_alter_on_delete_db_level(self):
class DBOnDeleteParent(Model):
class Meta:
app_label = "schema"

class DBOnDeleteChild(Model):
parent = ForeignKey(DBOnDeleteParent, DB_CASCADE, null=True)

class Meta:
app_label = "schema"

self.isolated_local_models = [DBOnDeleteChild, DBOnDeleteParent]
with connection.schema_editor() as editor:
editor.create_model(DBOnDeleteParent)
editor.create_model(DBOnDeleteChild)
# Changing between database-level on_delete options requires database
# changes.
old_field = DBOnDeleteChild._meta.get_field("parent")
new_field = ForeignKey(DBOnDeleteParent, DB_SET_NULL, null=True)
new_field.set_attributes_from_name("parent")
with (
connection.schema_editor() as editor,
CaptureQueriesContext(connection) as ctx,
):
editor.alter_field(DBOnDeleteChild, old_field, new_field, strict=True)
self.assertIs(
any("SET NULL" in query["sql"] for query in ctx.captured_queries), True
)

@isolate_apps("schema")
@skipUnlessDBFeature("supports_foreign_keys", "can_introspect_foreign_keys")
def test_create_model_db_on_delete(self):
Expand Down Expand Up @@ -4926,7 +4973,7 @@ def test_alter_field_fk_attributes_noop(self):
error_messages={"invalid": "error message"},
help_text="help text",
limit_choices_to={"limit": "choice"},
on_delete=CASCADE,
on_delete=PROTECT,
related_name="related_name",
related_query_name="related_query_name",
validators=[lambda x: x],
Expand Down
Loading