Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/main'
Browse files Browse the repository at this point in the history
  • Loading branch information
dan-mm committed Dec 30, 2023
2 parents 4ee6a7d + dc26a3d commit f4a5568
Show file tree
Hide file tree
Showing 11 changed files with 297 additions and 50 deletions.
83 changes: 39 additions & 44 deletions django/contrib/admin/static/admin/js/theme.js
Original file line number Diff line number Diff line change
@@ -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();
}
2 changes: 1 addition & 1 deletion django/contrib/admin/templates/admin/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<link rel="stylesheet" href="{% block stylesheet %}{% static "admin/css/base.css" %}{% endblock %}">
{% block dark-mode-vars %}
<link rel="stylesheet" href="{% static "admin/css/dark_mode.css" %}">
<script src="{% static "admin/js/theme.js" %}" defer></script>
<script src="{% static "admin/js/theme.js" %}"></script>
{% endblock %}
{% if not is_popup and is_nav_sidebar_enabled %}
<link rel="stylesheet" href="{% static "admin/css/nav_sidebar.css" %}">
Expand Down
14 changes: 11 additions & 3 deletions django/contrib/postgres/fields/array.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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:
Expand Down
60 changes: 60 additions & 0 deletions django/db/models/expressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
):
Expand Down Expand Up @@ -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."""
Expand Down
15 changes: 15 additions & 0 deletions django/db/models/fields/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/ref/forms/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -909,7 +909,7 @@ It's possible to customize that character, or omit it entirely, using the
<div><label for="id_for_cc_myself">Cc myself</label><input type="checkbox" name="cc_myself" id="id_for_cc_myself"></div>
>>> f = ContactForm(auto_id="id_for_%s", label_suffix=" ->")
>>> print(f)
<div><label for="id_for_subject">Subject:</label><input type="text" name="subject" maxlength="100" required id="id_for_subject"></div>
<div><label for="id_for_subject">Subject -&gt;</label><input type="text" name="subject" maxlength="100" required id="id_for_subject"></div>
<div><label for="id_for_message">Message -&gt;</label><textarea name="message" cols="40" rows="10" required id="id_for_message"></textarea></div>
<div><label for="id_for_sender">Sender -&gt;</label><input type="email" name="sender" required id="id_for_sender"></div>
<div><label for="id_for_cc_myself">Cc myself -&gt;</label><input type="checkbox" name="cc_myself" id="id_for_cc_myself"></div>
Expand Down
22 changes: 22 additions & 0 deletions docs/ref/models/expressions.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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()``
Expand Down
8 changes: 8 additions & 0 deletions docs/releases/5.1.txt
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,14 @@ Models
* :meth:`.QuerySet.order_by` now supports ordering by annotation transforms
such as ``JSONObject`` keys and ``ArrayAgg`` indices.

* :class:`F() <django.db.models.F>` and :class:`OuterRef()
<django.db.models.OuterRef>` expressions that output
:class:`~django.db.models.CharField`, :class:`~django.db.models.EmailField`,
:class:`~django.db.models.SlugField`, :class:`~django.db.models.URLField`,
:class:`~django.db.models.TextField`, or
:class:`~django.contrib.postgres.fields.ArrayField` can now be :ref:`sliced
<slicing-using-f>`.

Requests and Responses
~~~~~~~~~~~~~~~~~~~~~~

Expand Down
4 changes: 4 additions & 0 deletions tests/expressions/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,7 @@ class UUIDPK(models.Model):
class UUID(models.Model):
uuid = models.UUIDField(null=True)
uuid_fk = models.ForeignKey(UUIDPK, models.CASCADE, null=True)


class Text(models.Model):
name = models.TextField()
Loading

0 comments on commit f4a5568

Please sign in to comment.