From 9ea5f4facca9931519109ae5d3646d3077da3e5f Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Thu, 14 May 2026 13:25:52 -0400 Subject: [PATCH 1/5] Fixed minor typos and grammatical errors in docs and docstrings. Thanks to these people for pointing these out: * Hojeong Park * Natalia Bidart * Sulav Raj Bista --- django/contrib/admin/actions.py | 2 +- .../gis/db/backends/spatialite/operations.py | 2 +- django/contrib/gis/db/models/proxy.py | 2 +- django/contrib/gis/gdal/srs.py | 2 +- django/contrib/gis/utils/layermapping.py | 2 +- django/contrib/sessions/backends/db.py | 2 +- django/core/exceptions.py | 2 +- django/db/backends/ddl_references.py | 2 +- django/db/backends/mysql/operations.py | 2 +- django/db/backends/oracle/base.py | 2 +- django/db/backends/oracle/operations.py | 4 ++-- django/db/backends/postgresql/base.py | 2 +- django/db/backends/postgresql/psycopg_any.py | 2 +- django/db/backends/sqlite3/schema.py | 2 +- django/db/migrations/recorder.py | 2 +- django/db/models/base.py | 2 +- django/db/models/options.py | 2 +- django/db/models/sql/compiler.py | 2 +- django/db/models/sql/datastructures.py | 2 +- django/db/models/sql/where.py | 2 +- django/forms/models.py | 2 +- django/http/multipartparser.py | 2 +- django/template/context.py | 2 +- django/templatetags/tz.py | 2 +- django/utils/autoreload.py | 6 +++--- django/utils/encoding.py | 2 +- django/utils/regex_helper.py | 2 +- django/utils/translation/trans_real.py | 2 +- docs/conf.py | 2 +- .../writing-code/submitting-patches.txt | 16 ++++++++-------- docs/lint.py | 2 +- docs/ref/contrib/admin/filters.txt | 2 +- docs/ref/contrib/admin/index.txt | 4 ++-- docs/ref/contrib/auth.txt | 2 +- docs/ref/contrib/gis/commands.txt | 2 +- docs/ref/contrib/gis/db-api.txt | 2 +- docs/ref/contrib/gis/gdal.txt | 8 ++++---- docs/ref/contrib/gis/install/geolibs.txt | 2 +- docs/ref/contrib/gis/install/index.txt | 2 +- docs/ref/contrib/sitemaps.txt | 3 +-- docs/ref/contrib/sites.txt | 2 +- docs/ref/contrib/syndication.txt | 2 +- docs/ref/csp.txt | 2 +- docs/ref/files/file.txt | 2 +- docs/ref/forms/widgets.txt | 2 +- docs/ref/models/expressions.txt | 4 ++-- docs/ref/models/instances.txt | 2 +- docs/ref/models/querysets.txt | 2 +- docs/ref/utils.txt | 2 +- docs/releases/1.6.txt | 2 +- docs/topics/auth/passwords.txt | 2 +- docs/topics/cache.txt | 2 +- docs/topics/db/fetch-modes.txt | 2 +- docs/topics/db/multi-db.txt | 4 ++-- docs/topics/db/optimization.txt | 2 +- docs/topics/db/queries.txt | 2 +- docs/topics/files.txt | 4 ++-- docs/topics/forms/index.txt | 8 ++++---- docs/topics/testing/overview.txt | 2 +- docs/topics/testing/tools.txt | 2 +- tests/middleware/tests.py | 2 +- 61 files changed, 81 insertions(+), 82 deletions(-) diff --git a/django/contrib/admin/actions.py b/django/contrib/admin/actions.py index a2b92e492dbc..d709be8b2d6d 100644 --- a/django/contrib/admin/actions.py +++ b/django/contrib/admin/actions.py @@ -22,7 +22,7 @@ def delete_selected(modeladmin, request, queryset): This action first displays a confirmation page which shows all the deletable objects, or, if the user has no permission one of the related - childs (foreignkeys), a "permission denied" message. + children (foreignkeys), a "permission denied" message. Next, it deletes all selected objects and redirects back to the change list. diff --git a/django/contrib/gis/db/backends/spatialite/operations.py b/django/contrib/gis/db/backends/spatialite/operations.py index 51a04d5c502b..1cfd56964fd0 100644 --- a/django/contrib/gis/db/backends/spatialite/operations.py +++ b/django/contrib/gis/db/backends/spatialite/operations.py @@ -182,7 +182,7 @@ def rttopo_version(self): def geom_lib_version(self): """ - Return the version of the version-dependant geom library used by + Return the version of the version-dependent geom library used by SpatiaLite. """ if self.spatial_version >= (5,): diff --git a/django/contrib/gis/db/models/proxy.py b/django/contrib/gis/db/models/proxy.py index b415e147fc60..1b103aad6e4f 100644 --- a/django/contrib/gis/db/models/proxy.py +++ b/django/contrib/gis/db/models/proxy.py @@ -79,6 +79,6 @@ def __set__(self, instance, value): % (instance.__class__.__name__, gtype, type(value)) ) - # Setting the objects dictionary with the value, and returning. + # Setting the object's dictionary with the value, and returning. instance.__dict__[self.field.attname] = value return value diff --git a/django/contrib/gis/gdal/srs.py b/django/contrib/gis/gdal/srs.py index cd77a1bf6c79..4ca5337990f7 100644 --- a/django/contrib/gis/gdal/srs.py +++ b/django/contrib/gis/gdal/srs.py @@ -132,7 +132,7 @@ def __getitem__(self, target): 4326 >>> print(srs['TOWGS84', 4]) # the fourth value in this wkt 0 - >>> # For the units authority, have to use the pipe symbole. + >>> # For the units authority, have to use the pipe symbol. >>> print(srs['UNIT|AUTHORITY']) EPSG >>> print(srs['UNIT|AUTHORITY', 1]) # The authority value for the units diff --git a/django/contrib/gis/utils/layermapping.py b/django/contrib/gis/utils/layermapping.py index e55737885941..b3daf91a634e 100644 --- a/django/contrib/gis/utils/layermapping.py +++ b/django/contrib/gis/utils/layermapping.py @@ -590,7 +590,7 @@ def save( progress: When this keyword is set, status information will be printed giving the number of features processed and successfully saved. By default, - progress information will pe printed every 1000 features processed, + progress information will be printed every 1000 features processed, however, this default may be overridden by setting this keyword with an integer for the desired interval. diff --git a/django/contrib/sessions/backends/db.py b/django/contrib/sessions/backends/db.py index 6d6247d6c94b..0b8e96a71b37 100644 --- a/django/contrib/sessions/backends/db.py +++ b/django/contrib/sessions/backends/db.py @@ -144,7 +144,7 @@ async def asave(self, must_create=False): obj = await self.acreate_model_instance(data) using = router.db_for_write(self.model, instance=obj) try: - # This code MOST run in a transaction, so it requires + # This code MUST run in a transaction, so it requires # @sync_to_async wrapping until transaction.atomic() supports # async. @sync_to_async diff --git a/django/core/exceptions.py b/django/core/exceptions.py index 93b2bcb87fa5..db88fdbcc08c 100644 --- a/django/core/exceptions.py +++ b/django/core/exceptions.py @@ -254,7 +254,7 @@ class EmptyResultSet(Exception): class FullResultSet(Exception): - """A database query predicate is matches everything.""" + """A database query predicate that matches everything.""" pass diff --git a/django/db/backends/ddl_references.py b/django/db/backends/ddl_references.py index cb8d2defd20c..dc9661fcac22 100644 --- a/django/db/backends/ddl_references.py +++ b/django/db/backends/ddl_references.py @@ -1,6 +1,6 @@ """ Helpers to manipulate deferred DDL statements that might need to be adjusted or -discarded within when executing a migration. +discarded when executing a migration. """ from copy import deepcopy diff --git a/django/db/backends/mysql/operations.py b/django/db/backends/mysql/operations.py index 7dee707820f8..7926baad73e8 100644 --- a/django/db/backends/mysql/operations.py +++ b/django/db/backends/mysql/operations.py @@ -61,7 +61,7 @@ def date_extract_sql(self, lookup_type, sql, params): # EXTRACT returns 1-53 based on ISO-8601 for the week number. lookup_type = lookup_type.upper() if not self._extract_format_re.fullmatch(lookup_type): - raise ValueError(f"Invalid loookup type: {lookup_type!r}") + raise ValueError(f"Invalid lookup type: {lookup_type!r}") return f"EXTRACT({lookup_type} FROM {sql})", params def date_trunc_sql(self, lookup_type, sql, params, tzname=None): diff --git a/django/db/backends/oracle/base.py b/django/db/backends/oracle/base.py index 9114777030bb..82e2d52d6319 100644 --- a/django/db/backends/oracle/base.py +++ b/django/db/backends/oracle/base.py @@ -546,7 +546,7 @@ def _output_type_handler(cursor, name, defaultType, length, precision, scale): arraysize=cursor.arraysize, outconverter=outconverter, ) - # oracledb 2.0.0+ returns NLOB columns with IS JSON constraints as + # oracledb 2.0.0+ returns NCLOB columns with IS JSON constraints as # dicts. Use a no-op converter to avoid this. elif defaultType == Database.DB_TYPE_NCLOB: return cursor.var(Database.DB_TYPE_NCLOB, arraysize=cursor.arraysize) diff --git a/django/db/backends/oracle/operations.py b/django/db/backends/oracle/operations.py index d946e37fac38..2fe10f99f19e 100644 --- a/django/db/backends/oracle/operations.py +++ b/django/db/backends/oracle/operations.py @@ -97,7 +97,7 @@ def date_extract_sql(self, lookup_type, sql, params): else: lookup_type = lookup_type.upper() if not self._extract_format_re.fullmatch(lookup_type): - raise ValueError(f"Invalid loookup type: {lookup_type!r}") + raise ValueError(f"Invalid lookup type: {lookup_type!r}") # https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/EXTRACT-datetime.html return f"EXTRACT({lookup_type} FROM {sql})", params return extract_sql, (*params, extract_param) @@ -198,7 +198,7 @@ def time_extract_sql(self, lookup_type, sql, params): def time_trunc_sql(self, lookup_type, sql, params, tzname=None): # The implementation is similar to `datetime_trunc_sql` as both # `DateTimeField` and `TimeField` are stored as TIMESTAMP where - # the date part of the later is ignored. + # the date part of the latter is ignored. sql, params = self._convert_sql_to_tz(sql, params, tzname) trunc_param = None if lookup_type == "hour": diff --git a/django/db/backends/postgresql/base.py b/django/db/backends/postgresql/base.py index 16cf7f84e61f..9f1afeffa49b 100644 --- a/django/db/backends/postgresql/base.py +++ b/django/db/backends/postgresql/base.py @@ -228,7 +228,7 @@ def pool(self): **pool_options, ) # setdefault() ensures that multiple threads don't set this in - # parallel. Since we do not open the pool during it's init above, + # parallel. Since we do not open the pool during its init above, # this means that at worst during startup multiple threads generate # pool objects and the first to set it wins. self._connection_pools.setdefault(self.alias, pool) diff --git a/django/db/backends/postgresql/psycopg_any.py b/django/db/backends/postgresql/psycopg_any.py index b877afc68062..0229d52e7b1c 100644 --- a/django/db/backends/postgresql/psycopg_any.py +++ b/django/db/backends/postgresql/psycopg_any.py @@ -27,7 +27,7 @@ def mogrify(sql, params, connection): # Adapters. class BaseTzLoader(adapt.Loader): """ - Load a PostgreSQL timestamptz using the a specific timezone. + Load a PostgreSQL timestamptz using a specific timezone. The timezone can be None too, in which case it will be chopped. """ diff --git a/django/db/backends/sqlite3/schema.py b/django/db/backends/sqlite3/schema.py index 94e3fc5bc778..47edec8f1ccc 100644 --- a/django/db/backends/sqlite3/schema.py +++ b/django/db/backends/sqlite3/schema.py @@ -266,7 +266,7 @@ def is_self_referential(f): # Delete the old table to make way for the new self.delete_model(model, handle_autom2m=False) - # Rename the new table to take way for the old + # Rename the new table to make way for the old self.alter_db_table( new_model, new_model._meta.db_table, diff --git a/django/db/migrations/recorder.py b/django/db/migrations/recorder.py index cf0bd4a2ce20..77ad80ba6275 100644 --- a/django/db/migrations/recorder.py +++ b/django/db/migrations/recorder.py @@ -56,7 +56,7 @@ def migration_qs(self): def has_table(self): """Return True if the django_migrations table exists.""" # If the migrations table has already been confirmed to exist, don't - # recheck it's existence. + # recheck its existence. if self._has_table: return True # It hasn't been confirmed to exist, recheck. diff --git a/django/db/models/base.py b/django/db/models/base.py index c88a10118a6e..32ddfc4f54d0 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -1593,7 +1593,7 @@ def _perform_date_checks(self, date_checks): for model_class, lookup_type, field, unique_for in date_checks: lookup_kwargs = {} # there's a ticket to add a date lookup, we can remove this special - # case if that makes it's way in + # case if that makes its way in date = getattr(self, unique_for) if date is None: continue diff --git a/django/db/models/options.py b/django/db/models/options.py index 109780ac4941..1fcb37a470c3 100644 --- a/django/db/models/options.py +++ b/django/db/models/options.py @@ -226,7 +226,7 @@ def contribute_to_class(self, cls, name): if self.verbose_name_plural is None: self.verbose_name_plural = format_lazy("{}s", self.verbose_name) - # order_with_respect_and ordering are mutually exclusive. + # order_with_respect_to and ordering are mutually exclusive. self._ordering_clash = bool(self.ordering and self.order_with_respect_to) # Any leftover attributes must be invalid. diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py index 2ce6edefe5c6..ef188783d400 100644 --- a/django/db/models/sql/compiler.py +++ b/django/db/models/sql/compiler.py @@ -466,7 +466,7 @@ def _order_by_pairs(self): else: if self.query.combinator and self.select: # Don't use the first model's field because other - # combinated queries might define it differently. + # combined queries might define it differently. yield OrderBy(F(col), descending=descending), False else: # 'col' is of the form 'field' or 'field1__field2' or diff --git a/django/db/models/sql/datastructures.py b/django/db/models/sql/datastructures.py index b4eea2320fef..9f9e1c254151 100644 --- a/django/db/models/sql/datastructures.py +++ b/django/db/models/sql/datastructures.py @@ -69,7 +69,7 @@ def __init__( ) # Along which field (or ForeignObjectRel in the reverse join case) self.join_field = join_field - # Is this join nullabled? + # Is this join nullable? self.nullable = nullable self.filtered_relation = filtered_relation diff --git a/django/db/models/sql/where.py b/django/db/models/sql/where.py index 82f96aa6ecb2..58cd88f915dc 100644 --- a/django/db/models/sql/where.py +++ b/django/db/models/sql/where.py @@ -59,7 +59,7 @@ def split_having_qualify(self, negated=False, must_group_by=False): and self.contains_aggregate and not self.contains_over_clause ): - # It's must cheaper to short-circuit and stash everything in the + # It's much cheaper to short-circuit and stash everything in the # HAVING clause than split children if possible. return None, self, None where_parts = [] diff --git a/django/forms/models.py b/django/forms/models.py index 9686baa6f289..e16abc90fa84 100644 --- a/django/forms/models.py +++ b/django/forms/models.py @@ -1402,7 +1402,7 @@ def clean(self, value): return None # if there is no value act as we did before. return self.parent_instance - # ensure the we compare the values as equal types. + # ensure we compare the values as equal types. if self.to_field: orig = getattr(self.parent_instance, self.to_field) else: diff --git a/django/http/multipartparser.py b/django/http/multipartparser.py index 9048d81c1a36..f901084a1029 100644 --- a/django/http/multipartparser.py +++ b/django/http/multipartparser.py @@ -517,7 +517,7 @@ def _update_unget_history(self, num_bytes): """ Update the unget history as a sanity check to see if we've pushed back the same number of bytes in one chunk. If we keep ungetting the - same number of bytes many times (here, 50), we're mostly likely in an + same number of bytes many times (here, 50), we're most likely in an infinite loop of some sort. This is usually caused by a maliciously-malformed MIME request. """ diff --git a/django/template/context.py b/django/template/context.py index 60f16f9be0ed..c0697ed091d5 100644 --- a/django/template/context.py +++ b/django/template/context.py @@ -130,7 +130,7 @@ def flatten(self): def __eq__(self, other): """ - Compare two contexts by comparing theirs 'dicts' attributes. + Compare two contexts by comparing their 'dicts' attributes. """ if not isinstance(other, BaseContext): return NotImplemented diff --git a/django/templatetags/tz.py b/django/templatetags/tz.py index e8787ac10c3f..09a8aa4e33e2 100644 --- a/django/templatetags/tz.py +++ b/django/templatetags/tz.py @@ -51,7 +51,7 @@ def do_timezone(value, arg): if timezone.is_naive(value): default_timezone = timezone.get_default_timezone() value = timezone.make_aware(value, default_timezone) - # Filters must never raise exceptionsm, so catch everything. + # Filters must never raise exceptions, so catch everything. except Exception: return "" diff --git a/django/utils/autoreload.py b/django/utils/autoreload.py index 13019f7214b7..db60d866f159 100644 --- a/django/utils/autoreload.py +++ b/django/utils/autoreload.py @@ -464,7 +464,7 @@ def _watch_root(self, root): # changes, however, as this is currently an internal API, no files # will be being watched outside of sys.path. Fixing this by checking # inside watch_glob() and watch_dir() is expensive, instead this could - # could fall back to the StatReloader if this case is detected? For + # fall back to the StatReloader if this case is detected? For # now, watching its parent, if possible, is sufficient. if not root.exists(): if not root.parent.exists(): @@ -531,8 +531,8 @@ def _watch_glob(self, directory, patterns): """ Watch a directory with a specific glob. If the directory doesn't yet exist, attempt to watch the parent directory and amend the patterns to - include this. It's important this method isn't called more than one per - directory when updating all subscriptions. Subsequent calls will + include this. It's important this method isn't called more than once + per directory when updating all subscriptions. Subsequent calls will overwrite the named subscription, so it must include all possible glob expressions. """ diff --git a/django/utils/encoding.py b/django/utils/encoding.py index e57e2a2ba140..686edd8e402f 100644 --- a/django/utils/encoding.py +++ b/django/utils/encoding.py @@ -248,7 +248,7 @@ def filepath_to_uri(path): def get_system_encoding(): """ The encoding for the character type functions. Fallback to 'ascii' if the - #encoding is unsupported by Python or could not be determined. See tickets + encoding is unsupported by Python or could not be determined. See tickets #10335 and #5846. """ try: diff --git a/django/utils/regex_helper.py b/django/utils/regex_helper.py index b71298e3990f..34307e3f797b 100644 --- a/django/utils/regex_helper.py +++ b/django/utils/regex_helper.py @@ -135,7 +135,7 @@ def normalize(pattern): raise ValueError( "Non-reversible reg-exp portion: '(?P%s'" % ch ) - # We are in a named capturing group. Extra the name and + # This is a named capturing group. Extract the name and # then skip to the end. if ch == "<": terminal_char = ">" diff --git a/django/utils/translation/trans_real.py b/django/utils/translation/trans_real.py index abc147744212..6fb6517a1017 100644 --- a/django/utils/translation/trans_real.py +++ b/django/utils/translation/trans_real.py @@ -449,7 +449,7 @@ def npgettext(context, singular, plural, number): def all_locale_paths(): """ - Return a list of paths to user-provides languages files. + Return a list of paths to user-provided language files. """ globalpath = os.path.join( os.path.dirname(sys.modules[settings.__module__].__file__), "locale" diff --git a/docs/conf.py b/docs/conf.py index 0c18e4fcae99..864b9f70cace 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -33,7 +33,7 @@ # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.append(abspath(join(dirname(__file__), "_ext"))) -# Use the module to GitHub url resolver, but import it after the _ext directoy +# Use the module to GitHub url resolver, but import it after the _ext directory # it lives in has been added to sys.path. import github_links # NOQA diff --git a/docs/internals/contributing/writing-code/submitting-patches.txt b/docs/internals/contributing/writing-code/submitting-patches.txt index 845dda6ff9cc..cf2a74783ec7 100644 --- a/docs/internals/contributing/writing-code/submitting-patches.txt +++ b/docs/internals/contributing/writing-code/submitting-patches.txt @@ -15,7 +15,7 @@ If you are fixing a really trivial issue, for example changing a word in the documentation, the preferred way to provide the patch is using GitHub pull requests without a Trac ticket. -See the :doc:`/internals/contributing/writing-code/working-with-git` for more +See :doc:`/internals/contributing/writing-code/working-with-git` for more details on how to use pull requests. .. _claiming-tickets: @@ -34,7 +34,7 @@ If you have identified a contribution you want to make and you're capable of fixing it (as measured by your coding ability, knowledge of Django internals and time availability), claim it by following these steps: -* `Login using your GitHub account`_ or `create an account`_ in our ticket +* `Log in using your GitHub account`_ or `create an account`_ in our ticket system. If you have an account but have forgotten your password, you can reset it using the `password reset page`_. @@ -59,7 +59,7 @@ and time availability), claim it by following these steps: * Claim the ticket by clicking the "assign to" radio button in the "Action" section. Your username will be filled in the text box by default. -* Finally click the "Submit changes" button at the bottom to save. +* Finally, click the "Submit changes" button at the bottom to save. .. note:: If your change is not :ref:`trivial `, you have the option @@ -67,7 +67,7 @@ and time availability), claim it by following these steps: of your contribution. This ensures that the Django Software Foundation has clear license to your contribution. -.. _Login using your GitHub account: https://code.djangoproject.com/github/login +.. _Log in using your GitHub account: https://code.djangoproject.com/github/login .. _Create an account: https://www.djangoproject.com/accounts/register/ .. _password reset page: https://www.djangoproject.com/accounts/password/reset/ .. _Contributor License Agreement: https://www.djangoproject.com/foundation/cla/ @@ -99,8 +99,8 @@ In the case of small changes, such as typos in the documentation or small bugs that will only take a few minutes to fix, you don't need to jump through the hoops of claiming tickets. Submit your changes directly and you're done! -It is *always* acceptable, regardless whether someone has claimed it or not, to -link proposals to a ticket if you happen to have the changes ready. +It is *always* acceptable, regardless of whether someone has claimed it or not, +to link proposals to a ticket if you happen to have the changes ready. .. _patch-style: @@ -271,7 +271,7 @@ mechanism for proposing and collecting community input on major new features. Before considering writing a DEP, it is recommended to first open a discussion following the :ref:`process for suggesting new features `. This allows the community to provide feedback and helps refine the proposal. -Once the DEP is ready the :ref:`Steering Council ` votes on +Once the DEP is ready, the :ref:`Steering Council ` votes on whether to accept it. Some examples of DEPs that have been approved and fully implemented: @@ -451,7 +451,7 @@ set the "Triage Stage" on the corresponding Trac ticket to "Ready for checkin". If you've left comments for improvement on the pull request, please tick the appropriate flags on the Trac ticket based on the results of your review: "Patch needs improvement", "Needs documentation", and/or "Needs tests". As time -and interest permits, mergers do final reviews of "Ready for checkin" tickets +and interest permit, mergers do final reviews of "Ready for checkin" tickets and will either commit the changes or bump it back to "Accepted" if further work needs to be done. diff --git a/docs/lint.py b/docs/lint.py index bff7b5fe6a00..d572bf9777b6 100644 --- a/docs/lint.py +++ b/docs/lint.py @@ -124,7 +124,7 @@ def is_multiline_block_to_exclude(line): @sphinxlint_checker(".rst", enabled=False, rst_only=True) def check_python_domain_in_roles(file, lines, options=None): """ - :py: indicates the Python language domain. This means code writen in + :py: indicates the Python language domain. This means code written in Python, not Python built-ins in particular. Bad: :py:class:`email.message.EmailMessage` diff --git a/docs/ref/contrib/admin/filters.txt b/docs/ref/contrib/admin/filters.txt index d55e6fb946d9..d1eae972c640 100644 --- a/docs/ref/contrib/admin/filters.txt +++ b/docs/ref/contrib/admin/filters.txt @@ -178,7 +178,7 @@ allows to store:: By defining a filter using the ``__in`` lookup, it is possible to filter for any of a group of values. You need to override the ``expected_parameters`` -method, and the specify the ``lookup_kwargs`` attribute with the appropriate +method, and then specify the ``lookup_kwargs`` attribute with the appropriate field name. By default, multiple values in the query string will be separated with commas, but this can be customized via the ``list_separator`` attribute. The following example shows such a filter using the vertical-pipe character as diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index 0d449a52b6f5..a16da77937d6 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/docs/ref/contrib/admin/index.txt @@ -2767,8 +2767,8 @@ You can override many of the templates which the admin module uses to generate the various pages of an admin site. You can even override a few of these templates for a specific app, or a specific model. -Set up your projects admin template directories ------------------------------------------------ +Set up your project's admin template directories +------------------------------------------------ The admin template files are located in the :source:`django/contrib/admin/templates/admin` directory. diff --git a/docs/ref/contrib/auth.txt b/docs/ref/contrib/auth.txt index b09b55cdda00..bbe199eb042c 100644 --- a/docs/ref/contrib/auth.txt +++ b/docs/ref/contrib/auth.txt @@ -587,7 +587,7 @@ The following backends are available in :mod:`django.contrib.auth.backends`: permissions if ``obj is not None``. :meth:`with_perm` also allows an object to be passed as a parameter, but - unlike others methods it returns an empty queryset if ``obj is not None``. + unlike other methods it returns an empty queryset if ``obj is not None``. .. method:: authenticate(request, username=None, password=None, **kwargs) .. method:: aauthenticate(request, username=None, password=None, **kwargs) diff --git a/docs/ref/contrib/gis/commands.txt b/docs/ref/contrib/gis/commands.txt index 4686155a7063..f9dfd5b24ff3 100644 --- a/docs/ref/contrib/gis/commands.txt +++ b/docs/ref/contrib/gis/commands.txt @@ -80,4 +80,4 @@ of using ``ogrinspect`` :ref:`in the tutorial `. .. django-admin-option:: --srid SRID The SRID to use for the geometry field. If not set, ``ogrinspect`` attempts - to automatically determine of the SRID of the data source. + to automatically determine the SRID of the data source. diff --git a/docs/ref/contrib/gis/db-api.txt b/docs/ref/contrib/gis/db-api.txt index 8f062e503da1..43d237b64c4c 100644 --- a/docs/ref/contrib/gis/db-api.txt +++ b/docs/ref/contrib/gis/db-api.txt @@ -120,7 +120,7 @@ raster models: >>> dem = Elevation(name="Canyon", rast=rast) >>> dem.save() -Note that this equivalent to: +Note that this is equivalent to: .. code-block:: pycon diff --git a/docs/ref/contrib/gis/gdal.txt b/docs/ref/contrib/gis/gdal.txt index 540140c32b8a..6e42f7a9e08c 100644 --- a/docs/ref/contrib/gis/gdal.txt +++ b/docs/ref/contrib/gis/gdal.txt @@ -1266,7 +1266,7 @@ Raster Data Objects =================== ``GDALRaster`` ----------------- +-------------- :class:`GDALRaster` is a wrapper for the GDAL raster source object that supports reading data from a variety of GDAL-supported geospatial file formats @@ -1777,7 +1777,7 @@ bands: one for red, one for green, and one for blue. .. method:: color_interp(as_string=False) - The color interpretation for the band, as an integer between 0and 16. + The color interpretation for the band, as an integer between 0 and 16. If ``as_string`` is ``True``, the data type is returned as a string with the following possible values: ``GCI_Undefined``, ``GCI_GrayIndex``, ``GCI_PaletteIndex``, ``GCI_RedBand``, @@ -2099,8 +2099,8 @@ documentation`_ for more details. Compressed rasters ^^^^^^^^^^^^^^^^^^ -Instead decompressing the file and instantiating the resulting raster, GDAL can -directly access compressed files using the ``/vsizip/``, ``/vsigzip/``, or +Instead of decompressing the file and instantiating the resulting raster, GDAL +can directly access compressed files using the ``/vsizip/``, ``/vsigzip/``, or ``/vsitar/`` virtual filesystems: .. code-block:: pycon diff --git a/docs/ref/contrib/gis/install/geolibs.txt b/docs/ref/contrib/gis/install/geolibs.txt index 1df5ba0c7536..6e5cae69719a 100644 --- a/docs/ref/contrib/gis/install/geolibs.txt +++ b/docs/ref/contrib/gis/install/geolibs.txt @@ -302,7 +302,7 @@ the GDAL library. For example: For example, the PROJ string for the `Google projection (900913 or 3857) `_ requires the ``null`` grid file only included in the extra datum shifting files. It is easier - to install the shifting files now, then to have debug a problem caused + to install the shifting files now, than to debug a problem caused by their absence later. .. [#] Specifically, GeoDjango provides support for the `OGR `_ library, a component of diff --git a/docs/ref/contrib/gis/install/index.txt b/docs/ref/contrib/gis/install/index.txt index 28f483e18945..eaba7f3fafc8 100644 --- a/docs/ref/contrib/gis/install/index.txt +++ b/docs/ref/contrib/gis/install/index.txt @@ -362,7 +362,7 @@ keep this checked, as it is necessary to install :ref:`postgisasb`. .. note:: If installed successfully, the PostgreSQL server will run in the background - each time the system as started as a Windows service. A + each time the system is started as a Windows service. A :menuselection:`PostgreSQL 15` start menu group will created and contains shortcuts for the Application Stack Builder (ASB) as well as the 'SQL Shell', which will launch a ``psql`` command window. diff --git a/docs/ref/contrib/sitemaps.txt b/docs/ref/contrib/sitemaps.txt index 4ec72d70f44b..8251bd3c8c27 100644 --- a/docs/ref/contrib/sitemaps.txt +++ b/docs/ref/contrib/sitemaps.txt @@ -330,8 +330,7 @@ The sitemap framework provides a convenience class for a common case: of the sitemap. It may also have a ``date_field`` entry that specifies a date field for objects retrieved from the ``queryset``. This will be used for the :attr:`~Sitemap.lastmod` attribute and - :meth:`~Sitemap.get_latest_lastmod` methods in the in the - generated sitemap. + :meth:`~Sitemap.get_latest_lastmod` methods in the generated sitemap. The :attr:`~Sitemap.priority`, :attr:`~Sitemap.changefreq`, and :attr:`~Sitemap.protocol` keyword arguments allow specifying these diff --git a/docs/ref/contrib/sites.txt b/docs/ref/contrib/sites.txt index 93ce59f1778a..3b0bfd95740c 100644 --- a/docs/ref/contrib/sites.txt +++ b/docs/ref/contrib/sites.txt @@ -358,7 +358,7 @@ Put another way, these two statements are equivalent:: How did :class:`~django.contrib.sites.managers.CurrentSiteManager` know which field of ``Photo`` was the :class:`~django.contrib.sites.models.Site`? By default, -:class:`~django.contrib.sites.managers.CurrentSiteManager` looks for a +:class:`~django.contrib.sites.managers.CurrentSiteManager` looks for either a :class:`~django.db.models.ForeignKey` called ``site`` or a :class:`~django.db.models.ManyToManyField` called diff --git a/docs/ref/contrib/syndication.txt b/docs/ref/contrib/syndication.txt index f9968b6e8cfe..d5097ce8a835 100644 --- a/docs/ref/contrib/syndication.txt +++ b/docs/ref/contrib/syndication.txt @@ -188,7 +188,7 @@ put into those elements. in ``items()``, Django first tries calling the ``item_link()`` method on the :class:`~django.contrib.syndication.views.Feed` class. In a similar way to - the title and description, it is passed it a single parameter, + the title and description, it is passed a single parameter, ``item``. If that method doesn't exist, Django tries executing a ``get_absolute_url()`` method on that object. Both ``get_absolute_url()`` and ``item_link()`` should return the diff --git a/docs/ref/csp.txt b/docs/ref/csp.txt index d86c08237f64..a9254e3c1c65 100644 --- a/docs/ref/csp.txt +++ b/docs/ref/csp.txt @@ -57,7 +57,7 @@ configured using the following settings: protection against active threats. * Use *both* to maintain an enforced baseline while experimenting with changes. Even for well-established policies, continuing to collect reports - reports can help detect regressions, unexpected changes in behavior, or + can help detect regressions, unexpected changes in behavior, or potential tampering in production environments. .. _csp-reports: diff --git a/docs/ref/files/file.txt b/docs/ref/files/file.txt index 701b2e5b371f..648bade3a4ad 100644 --- a/docs/ref/files/file.txt +++ b/docs/ref/files/file.txt @@ -40,7 +40,7 @@ The ``File`` class object`. In these cases, this attribute may itself be a :class:`File` subclass (and not necessarily the same subclass). Whenever possible, use the attributes and methods of the subclass - itself rather than the those of the subclass's ``file`` attribute. + itself rather than those of the subclass's ``file`` attribute. .. attribute:: mode diff --git a/docs/ref/forms/widgets.txt b/docs/ref/forms/widgets.txt index c65063cd68a4..25f6079a0aeb 100644 --- a/docs/ref/forms/widgets.txt +++ b/docs/ref/forms/widgets.txt @@ -358,7 +358,7 @@ foundation for custom widgets. all checkboxes to be checked instead of at least one. Override this method in custom widgets that aren't compatible with - browser validation. For example, a WSYSIWG text editor widget backed by + browser validation. For example, a WYSIWYG text editor widget backed by a hidden ``textarea`` element may want to always return ``False`` to avoid browser validation on the hidden field. diff --git a/docs/ref/models/expressions.txt b/docs/ref/models/expressions.txt index 79c939fa382e..94093f70d19f 100644 --- a/docs/ref/models/expressions.txt +++ b/docs/ref/models/expressions.txt @@ -866,7 +866,7 @@ expression is applied. For example, if you sum over the rows in a partition, the first result is the value of the first row, the second is the sum of first and second row. -The ``frame`` parameter specifies which other rows that should be used in the +The ``frame`` parameter specifies which other rows should be used in the computation. See :ref:`window-frames` for details. For example, to annotate each movie with the average rating for the movies by @@ -1190,7 +1190,7 @@ calling the appropriate methods on the wrapped expression. .. method:: get_group_by_cols() - Responsible for returning the list of columns references by + Responsible for returning the list of columns referenced by this expression. ``get_group_by_cols()`` should be called on any nested expressions. ``F()`` objects, in particular, hold a reference to a column. diff --git a/docs/ref/models/instances.txt b/docs/ref/models/instances.txt index 5a9bf421d477..a11c948ddcfa 100644 --- a/docs/ref/models/instances.txt +++ b/docs/ref/models/instances.txt @@ -837,7 +837,7 @@ For example:: def get_absolute_url(self): return "/people/%i/" % self.id -While this code is correct and simple, it may not be the most portable way to +While this code is correct and simple, it may not be the most portable way to write this kind of method. The :func:`~django.urls.reverse` function is usually the best approach. diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index f40c1405690e..e2d6e12319c8 100644 --- a/docs/ref/models/querysets.txt +++ b/docs/ref/models/querysets.txt @@ -3159,7 +3159,7 @@ The output differs significantly between databases. ``explain()`` is supported by all built-in database backends except Oracle because an implementation there isn't straightforward. -The ``format`` parameter changes the output format from the databases's +The ``format`` parameter changes the output format from the database's default, which is usually text-based. PostgreSQL supports ``'TEXT'``, ``'JSON'``, ``'YAML'``, and ``'XML'`` formats. MariaDB and MySQL support ``'TEXT'`` (also called ``'TRADITIONAL'``) and ``'JSON'`` formats. MySQL diff --git a/docs/ref/utils.txt b/docs/ref/utils.txt index bbd69c8ebada..1ec0e17862f8 100644 --- a/docs/ref/utils.txt +++ b/docs/ref/utils.txt @@ -649,7 +649,7 @@ escaping HTML. .. function:: conditional_escape(text) - Similar to ``escape()``, except that it doesn't operate on preescaped + Similar to ``escape()``, except that it doesn't operate on pre-escaped strings, so it will not double escape. .. function:: format_html(format_string, *args, **kwargs) diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index cf6b10caddd1..98638261657b 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -890,7 +890,7 @@ Miscellaneous * Due to a change in the form validation workflow, :class:`~django.forms.TypedChoiceField` ``coerce`` method should always return a value present in the ``choices`` field attribute. That limitation - should be lift again in Django 1.7. + should be lifted again in Django 1.7. * There have been changes in the way timeouts are handled in cache backends. Explicitly passing in ``timeout=None`` no longer results in using the diff --git a/docs/topics/auth/passwords.txt b/docs/topics/auth/passwords.txt index fa5b0094a92e..8cdafa16c760 100644 --- a/docs/topics/auth/passwords.txt +++ b/docs/topics/auth/passwords.txt @@ -650,7 +650,7 @@ Django includes four validators: Validates that the password is not a common password. This converts the password to lowercase (to do a case-insensitive comparison) and checks it - against a list of 20,000 common password created by `Royce Williams + against a list of 20,000 common passwords created by `Royce Williams `_. The ``password_list_path`` can be set to the path of a custom file of diff --git a/docs/topics/cache.txt b/docs/topics/cache.txt index a44dbae134d8..9056f14e10fa 100644 --- a/docs/topics/cache.txt +++ b/docs/topics/cache.txt @@ -871,7 +871,7 @@ It is considered an error to specify a cache name that is not configured. .. function:: django.core.cache.utils.make_template_fragment_key(fragment_name, vary_on=None) If you want to obtain the cache key used for a cached fragment, you can use -``make_template_fragment_key``. ``fragment_name`` is the same as second +``make_template_fragment_key``. ``fragment_name`` is the same as the second argument to the ``cache`` template tag; ``vary_on`` is a list of all additional arguments passed to the tag. This function can be useful for invalidating or overwriting a cached item, for example: diff --git a/docs/topics/db/fetch-modes.txt b/docs/topics/db/fetch-modes.txt index da7a07a0d49c..bcd9d33b9583 100644 --- a/docs/topics/db/fetch-modes.txt +++ b/docs/topics/db/fetch-modes.txt @@ -109,7 +109,7 @@ memory leaks where some peer instances are discarded. Raises a :exc:`~django.core.exceptions.FieldFetchBlocked` exception. Using ``RAISE`` for the above example would raise an exception at the access of -``book.author`` access, like: +``book.author``, like: .. code-block:: python diff --git a/docs/topics/db/multi-db.txt b/docs/topics/db/multi-db.txt index 5e423d4981bb..b5474ee9ebe5 100644 --- a/docs/topics/db/multi-db.txt +++ b/docs/topics/db/multi-db.txt @@ -229,7 +229,7 @@ object instance that is related to the read or write operation that is underway. This might be the instance that is being saved, or it might be an instance that is being added in a many-to-many relation. In some cases, no instance hint will be provided at all. The router checks for -the existence of an instance hint, and determine if that hint should be +the existence of an instance hint, and determines if that hint should be used to alter routing behavior. If an ``instance`` hint is present, the database from which it was fetched @@ -417,7 +417,7 @@ catch-all nature of the PrimaryReplicaRouter implementation would mean that all models would be available on all databases. With this setup installed, and all databases migrated as per -:ref:`synchronizing_multiple_databases`, lets run some Django code: +:ref:`synchronizing_multiple_databases`, let's run some Django code: .. code-block:: pycon diff --git a/docs/topics/db/optimization.txt b/docs/topics/db/optimization.txt index ed990a123f24..311c8252e1db 100644 --- a/docs/topics/db/optimization.txt +++ b/docs/topics/db/optimization.txt @@ -207,7 +207,7 @@ Use the ``FETCH_PEERS`` fetch mode ---------------------------------- Use the :attr:`~django.db.models.FETCH_PEERS` fetch mode to make on-demand -field access more efficient with bulk-fetching. Enable all it for all usage of +field access more efficient with bulk-fetching. Enable it for all usage of your models :ref:`with a custom manager `. Using this fetch mode is easier than declaring fields to fetch with diff --git a/docs/topics/db/queries.txt b/docs/topics/db/queries.txt index e5a09a3a4e65..717dc388c598 100644 --- a/docs/topics/db/queries.txt +++ b/docs/topics/db/queries.txt @@ -2093,7 +2093,7 @@ The answer lies in the :data:`app registry `. When Django starts, it imports each application listed in :setting:`INSTALLED_APPS`, and then the ``models`` module inside each application. Whenever a new model class is created, Django adds backward-relationships to any related models. If the -related models haven't been imported yet, Django keeps tracks of the +related models haven't been imported yet, Django keeps track of the relationships and adds them when the related models eventually are imported. For this reason, it's particularly important that all the models you're using diff --git a/docs/topics/files.txt b/docs/topics/files.txt index e220dfac5f21..2ff8960a13b0 100644 --- a/docs/topics/files.txt +++ b/docs/topics/files.txt @@ -243,7 +243,7 @@ parameter for :class:`~django.db.models.FileField` or storage at runtime, selecting different storages for different environments, for example. -Your callable will be evaluated when your models classes are loaded, and must +Your callable will be evaluated when your model classes are loaded, and must return an instance of :class:`~django.core.files.storage.Storage`. For example:: @@ -273,7 +273,7 @@ use :data:`~django.core.files.storage.storages`:: class MyModel(models.Model): upload = models.FileField(storage=select_storage) -Because the callable is evaluated when your models classes are loaded, if you +Because the callable is evaluated when your model classes are loaded, if you need to override the :setting:`STORAGES` setting in tests, you should use a ``LazyObject`` subclass instead:: diff --git a/docs/topics/forms/index.txt b/docs/topics/forms/index.txt index 6d0b96b0b3c0..78082dd06df0 100644 --- a/docs/topics/forms/index.txt +++ b/docs/topics/forms/index.txt @@ -578,13 +578,13 @@ Reusable field group templates ------------------------------ Each field is available as an attribute of the form, using -``{{ form.name_of_field }}`` in a template. A field has a +``{{ form.name_of_field }}`` in a template. A field has an :meth:`~django.forms.BoundField.as_field_group` method which renders the -related elements of the field as a group, its label, widget, errors, and help +related elements of the field as a group: its label, widget, errors, and help text. -This allows generic templates to be written that arrange fields elements in the -required layout. For example: +This allows generic templates to be written that arrange each field's elements +in the required layout. For example: .. code-block:: html+django diff --git a/docs/topics/testing/overview.txt b/docs/topics/testing/overview.txt index 53d32a34d384..2b3b97283824 100644 --- a/docs/topics/testing/overview.txt +++ b/docs/topics/testing/overview.txt @@ -394,6 +394,6 @@ Using ``setUpTestData`` to create test data Moving common test data to :meth:`TestCase.setUpTestData` will reduce the number of times your tests need to create the data. Using -:meth:`TestCase.setUpTestData` to will create test data +:meth:`TestCase.setUpTestData` will create test data once per class. While :meth:`TestCase.setUp() ` will create the test data once per test. diff --git a/docs/topics/testing/tools.txt b/docs/topics/testing/tools.txt index 6eeb96817bec..976aa4c93373 100644 --- a/docs/topics/testing/tools.txt +++ b/docs/topics/testing/tools.txt @@ -444,7 +444,7 @@ Use the ``django.test.Client`` class to make requests. require different credentials. It requires whichever credentials are required by your backend's ``authenticate()`` method. - ``login()`` returns ``True`` if it the credentials were accepted and + ``login()`` returns ``True`` if the credentials were accepted and login was successful. Finally, you'll need to remember to create user accounts before you can diff --git a/tests/middleware/tests.py b/tests/middleware/tests.py index e8c30b744ace..4996e339355c 100644 --- a/tests/middleware/tests.py +++ b/tests/middleware/tests.py @@ -461,7 +461,7 @@ def is_ignorable_request(self, request, uri, domain, referer): def test_referer_equal_to_requested_url(self): """ Some bots set the referer to the current URL to avoid being blocked by - an referer check (#25302). + a referer check (#25302). """ self.req.META["HTTP_REFERER"] = self.req.path BrokenLinkEmailsMiddleware(self.get_response)(self.req) From 4a89f5c3978821e559b55f45d7a86984b26ceaa0 Mon Sep 17 00:00:00 2001 From: Mike Edmunds Date: Mon, 6 Jul 2026 16:52:28 -0700 Subject: [PATCH 2/5] Fixed typo and versionadded locations in email docs. * Moved versionadded annotation to top of `mailers` section. * Added versionadded to `mailers.default` to help avoid confusion over deprecated annotation in same entry. * Added missing "a" in `get_connection()` deprecation note. --- docs/topics/email.txt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/topics/email.txt b/docs/topics/email.txt index b8446d908877..6cb05f404984 100644 --- a/docs/topics/email.txt +++ b/docs/topics/email.txt @@ -1282,6 +1282,8 @@ backends. .. data:: mailers + .. versionadded:: 6.1 + You can access the mailers configured in the :setting:`MAILERS` setting through a dict-like object: ``django.core.mail.mailers``: @@ -1293,10 +1295,10 @@ backends. If the named key is not defined, a ``MailerDoesNotExist`` error will be raised. Other configuration problems will raise an ``InvalidMailer`` error. - .. versionadded:: 6.1 - .. data:: mailers.default + .. versionadded:: 6.1 + As a shortcut, the default mailer can be accessed through ``django.core.mail.mailers.default``: @@ -1348,7 +1350,7 @@ backends. .. deprecated:: 6.0 - Passing ``fail_silently`` as positional argument is deprecated. + Passing ``fail_silently`` as a positional argument is deprecated. .. deprecated:: 6.1 From e0d64a5adaaad1cec22a0f909602ea4127c04ad3 Mon Sep 17 00:00:00 2001 From: Mike Edmunds Date: Wed, 15 Jul 2026 17:02:39 -0700 Subject: [PATCH 3/5] Fixed subject/verb agreement in DecimalValidator messages. --- django/core/validators.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/django/core/validators.py b/django/core/validators.py index 673458aacfca..862140bcda84 100644 --- a/django/core/validators.py +++ b/django/core/validators.py @@ -513,17 +513,17 @@ class DecimalValidator: messages = { "invalid": _("Enter a number."), "max_digits": ngettext_lazy( - "Ensure that there are no more than %(max)s digit in total.", + "Ensure that there is no more than %(max)s digit in total.", "Ensure that there are no more than %(max)s digits in total.", "max", ), "max_decimal_places": ngettext_lazy( - "Ensure that there are no more than %(max)s decimal place.", + "Ensure that there is no more than %(max)s decimal place.", "Ensure that there are no more than %(max)s decimal places.", "max", ), "max_whole_digits": ngettext_lazy( - "Ensure that there are no more than %(max)s digit before the decimal " + "Ensure that there is no more than %(max)s digit before the decimal " "point.", "Ensure that there are no more than %(max)s digits before the decimal " "point.", From 8a162076e1988ddf9453edfb8329d5df1573dc38 Mon Sep 17 00:00:00 2001 From: Natalia <124304+nessita@users.noreply.github.com> Date: Fri, 17 Jul 2026 14:00:18 -0300 Subject: [PATCH 4/5] Improved wording in PR quality check messages. --- .github/pull_request_template.md | 2 +- scripts/pr_quality/check_pr.py | 6 +++--- scripts/pr_quality/errors.py | 4 ++-- scripts/pr_quality/tests/test_check_pr.py | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index d3a9d64ec1a8..98ec88c7233e 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -5,7 +5,7 @@ ticket-XXXXX #### Branch description - + #### AI Assistance Disclosure (REQUIRED) diff --git a/scripts/pr_quality/check_pr.py b/scripts/pr_quality/check_pr.py index 272e003567d8..98e14463cbf8 100644 --- a/scripts/pr_quality/check_pr.py +++ b/scripts/pr_quality/check_pr.py @@ -330,7 +330,7 @@ def check_pr_title_has_ticket(pr_title, ticket_id): def check_branch_description(pr_body): - """The branch description should be at least 5 words long.""" + """The branch description should be at least five words long.""" description_match = re.search( r"#### Branch description[ \t]*\r?\n(.*?)(?=\r?\n####|\Z)", pr_body, @@ -351,7 +351,7 @@ def check_branch_description(pr_body): def check_ai_disclosure(pr_body): """Exactly one AI disclosure checkbox must be selected. - If the "AI tools were used" option is checked, at least 5 words of + If the "AI tools were used" option is checked, at least five words of additional description must be present in that section. """ ai_match = re.search( @@ -381,7 +381,7 @@ def check_ai_disclosure(pr_body): for line in section.splitlines() if line.strip() and not line.strip().startswith("- [") ] - # Ensure PR author includes at least 5 words about their AI use. + # Ensure PR author includes at least five words about their AI use. if len(" ".join(extra_lines).split()) < MIN_WORDS: return Message(*MISSING_AI_DESCRIPTION) diff --git a/scripts/pr_quality/errors.py b/scripts/pr_quality/errors.py index eb75b556f5f8..90b7645e2fd3 100644 --- a/scripts/pr_quality/errors.py +++ b/scripts/pr_quality/errors.py @@ -74,7 +74,7 @@ def as_details(self, level=LEVEL_ERROR): "AI Tool Usage Not Described", "You indicated that AI tools were used in preparing this PR, but you have not " "provided a description of which tools you used or how you used them. At least " - "5 words of description are required.\n\n" + "five words of description are required.\n\n" "**What to do:**\n\n" "Add a brief description below the checked AI disclosure checkbox in your PR " "description. For example:\n\n" @@ -114,7 +114,7 @@ def as_details(self, level=LEVEL_ERROR): "Missing PR Description", "Your PR description must be substantive and meaningful.\n\n" "**What to do:**\n\n" - "Write a description that contains at least 5 words and addresses:\n\n" + "Write a description that contains at least five words and addresses:\n\n" "- What problem does this PR solve?\n" "- Why is this change necessary?\n" "- What approach did you take?\n\n" diff --git a/scripts/pr_quality/tests/test_check_pr.py b/scripts/pr_quality/tests/test_check_pr.py index 74b33500001f..757433c98601 100644 --- a/scripts/pr_quality/tests/test_check_pr.py +++ b/scripts/pr_quality/tests/test_check_pr.py @@ -673,7 +673,7 @@ def test_make_pr_body_matches_template(self): ticket="ticket-XXXXX", description=( "" + " the proposed changes. Minimum five words. -->" ), no_ai_checked=False, ai_used_checked=False, From 4ea38d54c10e0f44e189605c217a55cdfe9fdde8 Mon Sep 17 00:00:00 2001 From: Natalia <124304+nessita@users.noreply.github.com> Date: Wed, 10 Jun 2026 18:26:39 -0300 Subject: [PATCH 5/5] Fixed #37160 -- Made admin views raise PermissionDenied consistently. The admin view_on_site and history views now return an HTTP 403 response when a staff user lacks view or change permission for the target model, consistent with the changeform and autocomplete views. Thanks Bence Nagy for the report. --- django/contrib/admin/options.py | 6 +-- django/contrib/admin/sites.py | 26 ++++++++++--- docs/releases/6.2.txt | 10 +++++ tests/admin_views/tests.py | 68 +++++++++++++++++++++++++++++++++ 4 files changed, 101 insertions(+), 9 deletions(-) diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index fd4c644b048b..8ce7ffa94227 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -2563,14 +2563,14 @@ def history_view(self, request, object_id, extra_context=None): # First check if the user can see this history. model = self.model obj = self.get_object(request, unquote(object_id)) + if not self.has_view_or_change_permission(request, obj): + raise PermissionDenied + if obj is None: return self._get_obj_does_not_exist_redirect( request, model._meta, object_id ) - if not self.has_view_or_change_permission(request, obj): - raise PermissionDenied - # Then get the history for this object. app_label = self.opts.app_label action_list = ( diff --git a/django/contrib/admin/sites.py b/django/contrib/admin/sites.py index d160ec06d51d..9a46c8e39b61 100644 --- a/django/contrib/admin/sites.py +++ b/django/contrib/admin/sites.py @@ -9,7 +9,7 @@ from django.contrib.admin.views.autocomplete import AutocompleteJsonView from django.contrib.auth import REDIRECT_FIELD_NAME from django.contrib.auth.decorators import login_not_required -from django.core.exceptions import ImproperlyConfigured +from django.core.exceptions import ImproperlyConfigured, PermissionDenied from django.db.models.base import ModelBase from django.http import Http404, HttpResponsePermanentRedirect, HttpResponseRedirect from django.template.response import TemplateResponse @@ -256,10 +256,6 @@ def inner(request, *args, **kwargs): return update_wrapper(inner, view) def get_urls(self): - # Since this module gets imported in the application's root package, - # it cannot import models from other applications at the module level, - # and django.contrib.contenttypes.views imports ContentType. - from django.contrib.contenttypes import views as contenttype_views from django.urls import include, path, re_path def wrap(view, cacheable=False): @@ -290,7 +286,7 @@ def wrapper(*args, **kwargs): path("jsi18n/", wrap(self.i18n_javascript, cacheable=True), name="jsi18n"), path( "r///", - wrap(contenttype_views.shortcut), + wrap(self.shortcut_view), name="view_on_site", ), ] @@ -451,6 +447,24 @@ def login(self, request, extra_context=None): def autocomplete_view(self, request): return AutocompleteJsonView.as_view(admin_site=self)(request) + def shortcut_view(self, request, content_type_id, object_id): + from django.contrib.contenttypes import views as contenttype_views + from django.contrib.contenttypes.models import ContentType + + try: + content_type = ContentType.objects.get_for_id(int(content_type_id)) + except (ContentType.DoesNotExist, ValueError): + pass + else: + model_class = content_type.model_class() + if ( + model_class is not None + and self.is_registered(model_class) + and not self.get_model_admin(model_class).has_view_permission(request) + ): + raise PermissionDenied + return contenttype_views.shortcut(request, content_type_id, object_id) + @no_append_slash def catch_all_view(self, request, url): if settings.APPEND_SLASH and not url.endswith("/"): diff --git a/docs/releases/6.2.txt b/docs/releases/6.2.txt index 7fdbad6f665a..d04c8a3be458 100644 --- a/docs/releases/6.2.txt +++ b/docs/releases/6.2.txt @@ -266,6 +266,16 @@ backends. * ... +:mod:`django.contrib.admin` +--------------------------- + +* The admin ``view_on_site`` URL now consistently returns an HTTP 403 response + when a staff user lacks view or change permission for the target model. + +* The admin history view now checks permissions before object existence, + consistently returning an HTTP 403 response for staff users without the view + or change permission regardless of whether the object exists. + Miscellaneous ------------- diff --git a/tests/admin_views/tests.py b/tests/admin_views/tests.py index 59606e1a11ea..92392c43e9cf 100644 --- a/tests/admin_views/tests.py +++ b/tests/admin_views/tests.py @@ -3380,6 +3380,24 @@ def test_history_view_bad_url(self): ["article with ID “foo” doesn’t exist. Perhaps it was deleted?"], ) + def test_history_view_without_permission_returns_403(self): + self.client.force_login(self.adduser) + for label, pk in [("existing", self.a1.pk), ("missing", 999999)]: + with self.subTest(pk=label): + response = self.client.get( + reverse("admin:admin_views_article_history", args=(pk,)) + ) + self.assertEqual(response.status_code, 403) + + def test_history_view_with_view_or_change_permission_success(self): + for permission, user in [("view", self.viewuser), ("change", self.changeuser)]: + with self.subTest(permission=permission): + self.client.force_login(user) + response = self.client.get( + reverse("admin:admin_views_article_history", args=(self.a1.pk,)) + ) + self.assertEqual(response.status_code, 200) + def test_conditionally_show_add_section_link(self): """ The foreign key widget should only show the "add related" button if the @@ -3527,6 +3545,56 @@ def test_shortcut_view_only_available_to_staff(self): # Domain may depend on contrib.sites tests also run self.assertRegex(response.url, "http://(testserver|example.com)/dummy/foo/") + def test_shortcut_view_without_permission_returns_403(self): + obj = ModelWithStringPrimaryKey.objects.create(string_pk="bar") + model_ctype = ContentType.objects.get_for_model(ModelWithStringPrimaryKey) + shortcut_url = reverse("admin:view_on_site", args=(model_ctype.pk, obj.pk)) + # deleteuser has no view permission on ModelWithStringPrimaryKey. + self.client.force_login(self.deleteuser) + response = self.client.get(shortcut_url) + self.assertEqual(response.status_code, 403) + + def test_shortcut_view_with_view_or_change_permission_success(self): + obj = ModelWithStringPrimaryKey.objects.create(string_pk="bar") + model_ctype = ContentType.objects.get_for_model(ModelWithStringPrimaryKey) + shortcut_url = reverse("admin:view_on_site", args=(model_ctype.pk, obj.pk)) + opts = ModelWithStringPrimaryKey._meta + for permission in ["view", "change"]: + codename = get_permission_codename(permission, opts) + perm = get_perm(ModelWithStringPrimaryKey, codename) + with self.subTest(permission=permission): + self.viewuser.user_permissions.set([perm]) + self.client.force_login(self.viewuser) + response = self.client.get(shortcut_url) + self.assertEqual(response.status_code, 302) + + def test_shortcut_view_for_invalid_content_type_returns_404(self): + # An unknown or non-int content type id skips the permission check and + # falls back to the contenttypes shortcut view, which raises Http404. + self.client.force_login(self.deleteuser) + for content_type_id in [9999, "not-an-int", None]: + with self.subTest(content_type_id=content_type_id): + response = self.client.get( + reverse("admin:view_on_site", args=(content_type_id, 1)) + ) + self.assertEqual(response.status_code, 404) + + def test_shortcut_view_does_not_repeat_content_type_query(self): + obj = ModelWithStringPrimaryKey.objects.create(string_pk="bar") + model_ctype = ContentType.objects.get_for_model(ModelWithStringPrimaryKey) + shortcut_url = reverse("admin:view_on_site", args=(model_ctype.pk, obj.pk)) + self.client.force_login(self.superuser) + # A warmup request populates the ContentType and Site caches, so only + # relevant queries are measured. The 4 expected queries are: + # 1. Load the session. + # 2. Load the user. + # 3. Look up the content type. + # 4. Fetch the target instance. + self.client.get(shortcut_url) + with self.assertNumQueries(4): + response = self.client.get(shortcut_url) + self.assertEqual(response.status_code, 302) + def test_has_module_permission(self): """ has_module_permission() returns True for all users who