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
2 changes: 2 additions & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -668,6 +668,7 @@ answer newbie questions, and generally made Django that much better:
Mads Jensen <https://github.com/atombrella>
Makoto Tsuyuki <mtsuyuki@gmail.com>
Malcolm Tredinnick
Manas Madeshiya <manas112madeshiya@gmail.com>
Manav Agarwal <dpsman13016@gmail.com>
Manuel Saelices <msaelices@yaco.es>
Manuzhai
Expand Down Expand Up @@ -787,6 +788,7 @@ answer newbie questions, and generally made Django that much better:
Mushtaq Ali <mushtaak@gmail.com>
Mykola Zamkovoi <nickzam@gmail.com>
Nadège Michel <michel.nadege@gmail.com>
Naga Kartheek Reddy Kona <konanagakartheek@gmail.com>
Nagy Károly <charlie@rendszergazda.com>
Nasimul Haque <nasim.haque@gmail.com>
Nasir Hussain <nasirhjafri@gmail.com>
Expand Down
12 changes: 6 additions & 6 deletions django/contrib/contenttypes/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,12 +100,12 @@ def get_formset(self, request, obj=None, **kwargs):
fields = kwargs.pop("fields")
else:
fields = flatten_fieldsets(self.get_fieldsets(request, obj))
exclude = [*(self.exclude or []), *self.get_readonly_fields(request, obj)]
if (
self.exclude is None
and hasattr(self.form, "_meta")
and self.form._meta.exclude
):
excluded = self.get_exclude(request, obj)
exclude = [
*(excluded or []),
*self.get_readonly_fields(request, obj),
]
if excluded is None and hasattr(self.form, "_meta") and self.form._meta.exclude:
# Take the custom ModelForm's Meta.exclude into account only if the
# GenericInlineModelAdmin doesn't define its own.
exclude.extend(self.form._meta.exclude)
Expand Down
17 changes: 14 additions & 3 deletions django/core/management/commands/sendtestemail.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,30 @@ def add_arguments(self, parser):
action="store_true",
help="Send a test email to the addresses specified in settings.ADMINS.",
)
parser.add_argument(
"--using",
default=None,
help=(
"Specify the MAILERS alias to use for sending the test email. "
"Defaults to 'default'."
),
)

def handle(self, *args, **kwargs):
def handle(self, *args, using=None, **kwargs):
subject = "Test email from %s on %s" % (socket.gethostname(), timezone.now())

send_mail(
subject=subject,
message="If you're reading this, it was successful.",
from_email=None,
recipient_list=kwargs["email"],
using=using,
)

if kwargs["managers"]:
mail_managers(subject, "This email was sent to the site managers.")
mail_managers(
subject, "This email was sent to the site managers.", using=using
)

if kwargs["admins"]:
mail_admins(subject, "This email was sent to the site admins.")
mail_admins(subject, "This email was sent to the site admins.", using=using)
16 changes: 15 additions & 1 deletion django/db/backends/base/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,9 @@ def __init__(self, connection, collect_sql=False, atomic=True):
self.collect_sql = collect_sql
if self.collect_sql:
self.collected_sql = []
# Tables renamed while collecting SQL don't exist under their new
# name in the database, so introspection must target the old name.
self.collected_table_renames = {}
self.atomic_migration = self.connection.features.can_rollback_ddl and atomic

# State-managing methods
Expand Down Expand Up @@ -700,6 +703,14 @@ def alter_db_table(self, model, old_db_table, new_db_table):
"new_table": self.quote_name(new_db_table),
}
)
if self.collect_sql:
# The rename isn't executed, so later introspection of the new
# table name must be redirected to the still-existing old one,
# following any earlier rename of the same table in this batch.
existing_table = self.collected_table_renames.pop(
old_db_table, old_db_table
)
self.collected_table_renames[new_db_table] = existing_table
# Rename all references to the old table name.
for sql in self.deferred_sql:
if isinstance(sql, Statement):
Expand Down Expand Up @@ -2022,9 +2033,12 @@ def _constraint_names(
)
for name in column_names
]
table_name = model._meta.db_table
if self.collect_sql:
table_name = self.collected_table_renames.get(table_name, table_name)
with self.connection.cursor() as cursor:
constraints = self.connection.introspection.get_constraints(
cursor, model._meta.db_table
cursor, table_name
)
result = []
for name, infodict in constraints.items():
Expand Down
7 changes: 6 additions & 1 deletion django/test/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -865,10 +865,15 @@ async def aforce_login(self, user, backend=None):

def _get_backend(self):
from django.contrib.auth import load_backend
from django.contrib.auth.backends import BaseBackend

def overrides(backend, name):
base = getattr(BaseBackend, name)
return getattr(type(backend), name, base) is not base

for backend_path in settings.AUTHENTICATION_BACKENDS:
backend = load_backend(backend_path)
if hasattr(backend, "get_user"):
if overrides(backend, "get_user") or overrides(backend, "aget_user"):
return backend_path

def _login(self, user, backend=None):
Expand Down
37 changes: 22 additions & 15 deletions docs/internals/howto-release-django.txt
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,8 @@ permissions.
Linux, or ``md5`` and ``shasum`` on macOS)
* python

* A GPG key pair. Ensure that the private part of this key is securely stored.
The public part needs to be uploaded to your GitHub account, and also to the
Jenkins server running the "confirm release" job.
* A GPG key pair. Securely store the private part, and protect it with a
passphrase. The public part needs to be uploaded to your GitHub account.

.. admonition:: More than one GPG key

Expand All @@ -96,18 +95,16 @@ permissions.
``you@example.com`` is the email address associated with the key you want
to use.

* A clean Python virtual environment (Python 3.9+) to build artifacts, with
these required Python packages installed:

.. code-block:: shell

$ python -m pip install build twine
* A clean Python virtual environment (Python 3.10+, pip 26.1+) to build
artifacts.

* Access to `Django's project on PyPI <https://pypi.org/project/Django/>`_ to
upload binaries, ideally with extra permissions to `yank a release
<https://pypi.org/help/#yanked>`_ if necessary. Create a project-scoped token
following the `official documentation <https://pypi.org/help/#apitoken>`_
and set up your ``$HOME/.pypirc`` file like this:
<https://pypi.org/help/#yanked>`_ if necessary. Ensure your PyPI account
only uses WebAuthn-based authentication factors, not TOTP (one-time codes).
Create an API token following the `official documentation
<https://pypi.org/help/#apitoken>`_, and set up your ``$HOME/.pypirc`` file
like this:

.. code-block:: ini
:caption: ``~/.pypirc``
Expand Down Expand Up @@ -451,10 +448,15 @@ issuing **multiple releases**, repeat these steps for each release.
release date for all releases, if necessary (:commit:`example commit
<34a503162fe222033a1cd3249bccad014fcd1d20>`).

#. Regenerate a fresh, dedicated virtual environment for the release tools
using a cooldown:

.. code-block:: shell

$ python -m pip install build twine --uploaded-prior-to=P7D

#. A release always begins from a release branch, so you should make sure
you're on an up-to-date stable branch. Also, you should have available a
clean and dedicated virtual environment per version being released. For
example:
you're on an up-to-date stable branch. For example:

.. code-block:: shell

Expand Down Expand Up @@ -642,12 +644,17 @@ Now you're ready to actually put the release out there. To do this:

__ https://djangoci.com/job/confirm-release/

#. Generate a new token for this release on PyPI, and store it in ``.pypirc``
as shown above.

#. Upload the release packages to PyPI:

.. code-block:: shell

$ twine upload --repository django dist/*

#. On PyPI, revoke the token you just created.

#. Update the newly created ``Release`` in the admin in ``djangoproject.com``
and enable the ``is_active`` flag.

Expand Down
1 change: 1 addition & 0 deletions docs/ref/contrib/admin/index.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2316,6 +2316,7 @@ adds some of its own (the shared features are actually defined in the
- :attr:`~ModelAdmin.filter_vertical`
- :attr:`~ModelAdmin.ordering`
- :attr:`~ModelAdmin.prepopulated_fields`
- :meth:`~ModelAdmin.get_exclude`
- :meth:`~ModelAdmin.get_fieldsets`
- :meth:`~ModelAdmin.get_queryset`
- :attr:`~ModelAdmin.radio_fields`
Expand Down
11 changes: 9 additions & 2 deletions docs/ref/django-admin.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1069,8 +1069,15 @@ recipient(s) specified. For example:

django-admin sendtestemail foo@example.com bar@example.com

There are a couple of options, and you may use any combination of them
together:
The following options may be used in any combination with each other and with
recipient email arguments.

.. django-admin-option:: --using ALIAS

.. versionadded:: 6.1

Specifies the :setting:`MAILERS` alias to use for sending the test email to
the recipients given as arguments. Defaults to the ``"default"`` mailer.

.. django-admin-option:: --managers

Expand Down
3 changes: 3 additions & 0 deletions docs/releases/6.1.txt
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,9 @@ Management Commands
:data:`~django.db.models.signals.m2m_changed` signals with ``raw=True`` when
loading fixtures.

* The :djadmin:`sendtestemail` command now supports a :option:`--using
<sendtestemail --using>` option to specify the :setting:`MAILERS` alias.

Models
~~~~~~

Expand Down
4 changes: 3 additions & 1 deletion docs/releases/6.2.txt
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,9 @@ Templates
Tests
~~~~~

* ...
* :meth:`~django.test.Client.force_login` now skips members of
:setting:`AUTHENTICATION_BACKENDS` not implementing ``(a)get_user()``, e.g.
permission-only backends.

URLs
~~~~
Expand Down
1 change: 1 addition & 0 deletions docs/spelling_wordlist
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ conf
config
contenttypes
contrib
cooldown
coroutine
coroutines
counterintuitive
Expand Down
6 changes: 4 additions & 2 deletions docs/topics/cache.txt
Original file line number Diff line number Diff line change
Expand Up @@ -253,8 +253,10 @@ In this example, the cache table's name is ``my_cache_table``::
}

Unlike other cache backends, the database cache does not support automatic
culling of expired entries at the database level. Instead, expired cache
entries are culled when an ``add()``, ``set()``, or ``touch()`` is called.
culling of expired entries at the database level. Instead, an expired cache
entry is deleted when evaluated by a ``get()`` request. Furthermore,
expired cache entries are culled when ``add()``, ``set()``, or ``touch()``
is called and the number of entries exceeds ``MAX_ENTRIES``.

Since the cull operation can be expensive for a large cache, you may control
how often this check occurs by setting ``CULL_PROBABILITY`` to a value between
Expand Down
12 changes: 9 additions & 3 deletions docs/topics/testing/tools.txt
Original file line number Diff line number Diff line change
Expand Up @@ -479,15 +479,21 @@ Use the ``django.test.Client`` class to make requests.

The user will have its ``backend`` attribute set to the value of the
``backend`` argument (which should be a dotted Python path string), or
to ``settings.AUTHENTICATION_BACKENDS[0]`` if a value isn't provided.
The :func:`~django.contrib.auth.authenticate` function called by
:meth:`login` normally annotates the user like this.
if a value isn't provided, the first backend from
:setting:`AUTHENTICATION_BACKENDS` implementing ``get_user()`` or
``aget_user()``. The :func:`~django.contrib.auth.authenticate` function
called by :meth:`login` normally annotates the user like this.

This method is faster than ``login()`` since the expensive
password hashing algorithms are bypassed. Also, you can speed up
``login()`` by :ref:`using a weaker hasher while testing
<speeding-up-tests-auth-hashers>`.

.. versionchanged:: 6.2

On older versions, backends not implementing ``(a)get_user()`` were
eligible to be selected.

.. method:: Client.logout()
.. method:: Client.alogout()

Expand Down
11 changes: 11 additions & 0 deletions tests/generic_inline_admin/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -577,3 +577,14 @@ def get_inlines(self, request, obj):
request.name = name
self.assertEqual(ma.get_inlines(request, None), (inline_class,))
self.assertEqual(type(ma.get_inline_instances(request)[0]), inline_class)

def test_get_exclude_is_respected(self):
class GetExcludeInline(GenericTabularInline):
model = Media

def get_exclude(self, request, obj=None):
return ["url"]

ma = GetExcludeInline(Media, self.site)
formset = ma.get_formset(request)
self.assertNotIn("url", formset.form.base_fields)
35 changes: 34 additions & 1 deletion tests/mail/test_sendtestemail.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
from django.core import mail
from django.core.mail import MailerDoesNotExist
from django.core.management import CommandError, call_command
from django.test import SimpleTestCase, override_settings


@override_settings(
ADMINS=["admin@example.com", "admin_and_manager@example.com"],
MANAGERS=["manager@example.com", "admin_and_manager@example.com"],
MAILERS={"default": {"BACKEND": "django.core.mail.backends.locmem.EmailBackend"}},
MAILERS={
"default": {"BACKEND": "django.core.mail.backends.locmem.EmailBackend"},
"notifications": {"BACKEND": "django.core.mail.backends.locmem.EmailBackend"},
},
)
class SendTestEmailManagementCommand(SimpleTestCase):
"""
Expand Down Expand Up @@ -108,3 +112,32 @@ def test_manager_and_admin_receivers(self):
"admin_and_manager@example.com",
],
)

def test_using_option(self):
recipient = "joe@example.com"
call_command("sendtestemail", "--using", "notifications", recipient)
self.assertEqual(len(mail.outbox), 1)
mail_message = mail.outbox[0]
self.assertEqual(mail_message.sent_using, "notifications")

def test_using_default(self):
recipient = "joe@example.com"
call_command("sendtestemail", recipient)
self.assertEqual(len(mail.outbox), 1)
mail_message = mail.outbox[0]
self.assertEqual(mail_message.sent_using, "default")

def test_using_option_with_managers(self):
call_command("sendtestemail", "--using", "notifications", "--managers")
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].sent_using, "notifications")

def test_using_option_with_admins(self):
call_command("sendtestemail", "--using", "notifications", "--admins")
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].sent_using, "notifications")

def test_using_nonexistent_mailer(self):
msg = "The mailer 'nonexistent' is not configured."
with self.assertRaisesMessage(MailerDoesNotExist, msg):
call_command("sendtestemail", "--using", "nonexistent", "joe@example.com")
Loading
Loading