diff --git a/django/contrib/admin/static/admin/js/theme.js b/django/contrib/admin/static/admin/js/theme.js index 794cd15f701a..e79d375c55cf 100644 --- a/django/contrib/admin/static/admin/js/theme.js +++ b/django/contrib/admin/static/admin/js/theme.js @@ -1,56 +1,51 @@ 'use strict'; { - window.addEventListener('load', function(e) { - - function setTheme(mode) { - if (mode !== "light" && mode !== "dark" && mode !== "auto") { - console.error(`Got invalid theme mode: ${mode}. Resetting to auto.`); - mode = "auto"; - } - document.documentElement.dataset.theme = mode; - localStorage.setItem("theme", mode); + function setTheme(mode) { + if (mode !== "light" && mode !== "dark" && mode !== "auto") { + console.error(`Got invalid theme mode: ${mode}. Resetting to auto.`); + mode = "auto"; } + document.documentElement.dataset.theme = mode; + localStorage.setItem("theme", mode); + } - function cycleTheme() { - const currentTheme = localStorage.getItem("theme") || "auto"; - const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches; + function cycleTheme() { + const currentTheme = localStorage.getItem("theme") || "auto"; + const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches; - if (prefersDark) { - // Auto (dark) -> Light -> Dark - if (currentTheme === "auto") { - setTheme("light"); - } else if (currentTheme === "light") { - setTheme("dark"); - } else { - setTheme("auto"); - } + if (prefersDark) { + // Auto (dark) -> Light -> Dark + if (currentTheme === "auto") { + setTheme("light"); + } else if (currentTheme === "light") { + setTheme("dark"); } else { - // Auto (light) -> Dark -> Light - if (currentTheme === "auto") { - setTheme("dark"); - } else if (currentTheme === "dark") { - setTheme("light"); - } else { - setTheme("auto"); - } + setTheme("auto"); + } + } else { + // Auto (light) -> Dark -> Light + if (currentTheme === "auto") { + setTheme("dark"); + } else if (currentTheme === "dark") { + setTheme("light"); + } else { + setTheme("auto"); } } + } - function initTheme() { - // set theme defined in localStorage if there is one, or fallback to auto mode - const currentTheme = localStorage.getItem("theme"); - currentTheme ? setTheme(currentTheme) : setTheme("auto"); - } - - function setupTheme() { - // Attach event handlers for toggling themes - const buttons = document.getElementsByClassName("theme-toggle"); - Array.from(buttons).forEach((btn) => { - btn.addEventListener("click", cycleTheme); - }); - initTheme(); - } + function initTheme() { + // set theme defined in localStorage if there is one, or fallback to auto mode + const currentTheme = localStorage.getItem("theme"); + currentTheme ? setTheme(currentTheme) : setTheme("auto"); + } - setupTheme(); + window.addEventListener('load', function(_) { + const buttons = document.getElementsByClassName("theme-toggle"); + Array.from(buttons).forEach((btn) => { + btn.addEventListener("click", cycleTheme); + }); }); + + initTheme(); } diff --git a/django/contrib/admin/templates/admin/base.html b/django/contrib/admin/templates/admin/base.html index 95494187294c..1ca50e508d1d 100644 --- a/django/contrib/admin/templates/admin/base.html +++ b/django/contrib/admin/templates/admin/base.html @@ -6,7 +6,7 @@ {% block dark-mode-vars %} - + {% endblock %} {% if not is_popup and is_nav_sidebar_enabled %} diff --git a/django/contrib/postgres/fields/array.py b/django/contrib/postgres/fields/array.py index c8e8e132e01d..4171af82f9d2 100644 --- a/django/contrib/postgres/fields/array.py +++ b/django/contrib/postgres/fields/array.py @@ -234,6 +234,12 @@ def formfield(self, **kwargs): } ) + def slice_expression(self, expression, start, length): + # If length is not provided, don't specify an end to slice to the end + # of the array. + end = None if length is None else start + length - 1 + return SliceTransform(start, end, expression) + class ArrayRHSMixin: def __init__(self, lhs, rhs): @@ -351,9 +357,11 @@ def __init__(self, start, end, *args, **kwargs): def as_sql(self, compiler, connection): lhs, params = compiler.compile(self.lhs) - if not lhs.endswith("]"): - lhs = "(%s)" % lhs - return "%s[%%s:%%s]" % lhs, (*params, self.start, self.end) + # self.start is set to 1 if slice start is not provided. + if self.end is None: + return f"({lhs})[%s:]", (*params, self.start) + else: + return f"({lhs})[%s:%s]", (*params, self.start, self.end) class SliceTransformFactory: diff --git a/django/db/models/expressions.py b/django/db/models/expressions.py index b67a2418d449..c20de5995a34 100644 --- a/django/db/models/expressions.py +++ b/django/db/models/expressions.py @@ -851,6 +851,9 @@ def __init__(self, name): def __repr__(self): return "{}({})".format(self.__class__.__name__, self.name) + def __getitem__(self, subscript): + return Sliced(self, subscript) + def resolve_expression( self, query=None, allow_joins=True, reuse=None, summarize=False, for_save=False ): @@ -925,6 +928,63 @@ def relabeled_clone(self, relabels): return self +class Sliced(F): + """ + An object that contains a slice of an F expression. + + Object resolves the column on which the slicing is applied, and then + applies the slicing if possible. + """ + + def __init__(self, obj, subscript): + super().__init__(obj.name) + self.obj = obj + if isinstance(subscript, int): + if subscript < 0: + raise ValueError("Negative indexing is not supported.") + self.start = subscript + 1 + self.length = 1 + elif isinstance(subscript, slice): + if (subscript.start is not None and subscript.start < 0) or ( + subscript.stop is not None and subscript.stop < 0 + ): + raise ValueError("Negative indexing is not supported.") + if subscript.step is not None: + raise ValueError("Step argument is not supported.") + if subscript.stop and subscript.start and subscript.stop < subscript.start: + raise ValueError("Slice stop must be greater than slice start.") + self.start = 1 if subscript.start is None else subscript.start + 1 + if subscript.stop is None: + self.length = None + else: + self.length = subscript.stop - (subscript.start or 0) + else: + raise TypeError("Argument to slice must be either int or slice instance.") + + def __repr__(self): + start = self.start - 1 + stop = None if self.length is None else start + self.length + subscript = slice(start, stop) + return f"{self.__class__.__qualname__}({self.obj!r}, {subscript!r})" + + def resolve_expression( + self, + query=None, + allow_joins=True, + reuse=None, + summarize=False, + for_save=False, + ): + resolved = query.resolve_ref(self.name, allow_joins, reuse, summarize) + if isinstance(self.obj, (OuterRef, self.__class__)): + expr = self.obj.resolve_expression( + query, allow_joins, reuse, summarize, for_save + ) + else: + expr = resolved + return resolved.output_field.slice_expression(expr, self.start, self.length) + + @deconstructible(path="django.db.models.Func") class Func(SQLiteNumericMixin, Expression): """An SQL function call.""" diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py index 41735d3b7f18..5186f0c414dd 100644 --- a/django/db/models/fields/__init__.py +++ b/django/db/models/fields/__init__.py @@ -15,6 +15,7 @@ from django.db import connection, connections, router from django.db.models.constants import LOOKUP_SEP from django.db.models.query_utils import DeferredAttribute, RegisterLookupMixin +from django.db.utils import NotSupportedError from django.utils import timezone from django.utils.choices import ( BlankChoiceIterator, @@ -1143,6 +1144,10 @@ def value_from_object(self, obj): """Return the value of this field in the given model instance.""" return getattr(obj, self.attname) + def slice_expression(self, expression, start, length): + """Return a slice of this field.""" + raise NotSupportedError("This field does not support slicing.") + class BooleanField(Field): empty_strings_allowed = False @@ -1303,6 +1308,11 @@ def deconstruct(self): kwargs["db_collation"] = self.db_collation return name, path, args, kwargs + def slice_expression(self, expression, start, length): + from django.db.models.functions import Substr + + return Substr(expression, start, length) + class CommaSeparatedIntegerField(CharField): default_validators = [validators.validate_comma_separated_integer_list] @@ -2497,6 +2507,11 @@ def deconstruct(self): kwargs["db_collation"] = self.db_collation return name, path, args, kwargs + def slice_expression(self, expression, start, length): + from django.db.models.functions import Substr + + return Substr(expression, start, length) + class TimeField(DateTimeCheckMixin, Field): empty_strings_allowed = False diff --git a/docs/ref/forms/api.txt b/docs/ref/forms/api.txt index 7bec9b120b9e..28cd452c4e8b 100644 --- a/docs/ref/forms/api.txt +++ b/docs/ref/forms/api.txt @@ -909,7 +909,7 @@ It's possible to customize that character, or omit it entirely, using the
>>> f = ContactForm(auto_id="id_for_%s", label_suffix=" ->") >>> print(f) - + diff --git a/docs/ref/models/expressions.txt b/docs/ref/models/expressions.txt index 9d85442d9ca6..67baef7dfc26 100644 --- a/docs/ref/models/expressions.txt +++ b/docs/ref/models/expressions.txt @@ -183,6 +183,28 @@ the field value of each one, and saving each one back to the database:: * getting the database, rather than Python, to do work * reducing the number of queries some operations require +.. _slicing-using-f: + +Slicing ``F()`` expressions +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. versionadded:: 5.1 + +For string-based fields, text-based fields, and +:class:`~django.contrib.postgres.fields.ArrayField`, you can use Python's +array-slicing syntax. The indices are 0-based and the ``step`` argument to +``slice`` is not supported. For example: + +.. code-block:: pycon + + >>> # Replacing a name with a substring of itself. + >>> writer = Writers.objects.get(name="Priyansh") + >>> writer.name = F("name")[1:5] + >>> writer.save() + >>> writer.refresh_from_db() + >>> writer.name + 'riya' + .. _avoiding-race-conditions-using-f: Avoiding race conditions using ``F()`` diff --git a/docs/releases/5.1.txt b/docs/releases/5.1.txt index cc72346eef01..b825e9be4f2a 100644 --- a/docs/releases/5.1.txt +++ b/docs/releases/5.1.txt @@ -184,6 +184,14 @@ Models * :meth:`.QuerySet.order_by` now supports ordering by annotation transforms such as ``JSONObject`` keys and ``ArrayAgg`` indices. +* :class:`F()