diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 9b88b9be9316..f61692ef7964 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -30,7 +30,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: '3.12' + python-version: '3.13' cache: 'pip' cache-dependency-path: 'docs/requirements.txt' - run: python -m pip install -r docs/requirements.txt @@ -48,7 +48,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: '3.12' + python-version: '3.13' - run: python -m pip install blacken-docs - name: Build docs run: | diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml index 3875e755f912..7c64dc98ff4c 100644 --- a/.github/workflows/linters.yml +++ b/.github/workflows/linters.yml @@ -27,7 +27,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: '3.12' + python-version: '3.13' - run: python -m pip install flake8 - name: flake8 # Pinned to v3.0.0. @@ -44,7 +44,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: '3.12' + python-version: '3.13' - run: python -m pip install isort - name: isort # Pinned to v3.0.0. diff --git a/.github/workflows/python_matrix.yml b/.github/workflows/python_matrix.yml new file mode 100644 index 000000000000..ab48c2be8322 --- /dev/null +++ b/.github/workflows/python_matrix.yml @@ -0,0 +1,52 @@ +name: Python Matrix from config file + +on: + pull_request: + types: [labeled, synchronize, opened, reopened] + paths-ignore: + - 'docs/**' + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + define-matrix: + if: contains(github.event.pull_request.labels.*.name, 'python-matrix') + runs-on: ubuntu-latest + outputs: + python_versions_output: ${{ steps.set-matrix.outputs.python_versions }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + - id: set-matrix + run: | + python_versions=$(sed -n "s/^.*Programming Language :: Python :: \([[:digit:]]\+\.[[:digit:]]\+\).*$/'\1', /p" pyproject.toml | tr -d '\n' | sed 's/, $//g') + echo "Supported Python versions: $python_versions" + echo "python_versions=[$python_versions]" >> "$GITHUB_OUTPUT" + python: + runs-on: ubuntu-latest + needs: define-matrix + strategy: + matrix: + python-version: ${{ fromJson(needs.define-matrix.outputs.python_versions_output) }} + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + cache-dependency-path: 'tests/requirements/py3.txt' + - name: Install libmemcached-dev for pylibmc + run: sudo apt-get install libmemcached-dev + - name: Install and upgrade packaging tools + run: python -m pip install --upgrade pip setuptools wheel + - run: python -m pip install -r tests/requirements/py3.txt -e . + - name: Run tests + run: python tests/runtests.py -v2 diff --git a/.github/workflows/schedule_tests.yml b/.github/workflows/schedule_tests.yml index c4523af4a030..495a61637846 100644 --- a/.github/workflows/schedule_tests.yml +++ b/.github/workflows/schedule_tests.yml @@ -19,7 +19,7 @@ jobs: - '3.10' - '3.11' - '3.12' - - '3.13-dev' + - '3.13' name: Windows, SQLite, Python ${{ matrix.python-version }} continue-on-error: true steps: @@ -46,7 +46,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: '3.12' + python-version: '3.13' cache: 'pip' - name: Install libmemcached-dev for pylibmc run: sudo apt-get install libmemcached-dev @@ -145,7 +145,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: '3.12' + python-version: '3.13' cache: 'pip' cache-dependency-path: 'tests/requirements/py3.txt' - name: Install libmemcached-dev for pylibmc @@ -181,7 +181,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: '3.12' + python-version: '3.13' cache: 'pip' cache-dependency-path: 'tests/requirements/py3.txt' - name: Install libmemcached-dev for pylibmc diff --git a/.github/workflows/selenium.yml b/.github/workflows/selenium.yml index fa916a0dedf0..bb82eb4203c5 100644 --- a/.github/workflows/selenium.yml +++ b/.github/workflows/selenium.yml @@ -24,7 +24,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: '3.12' + python-version: '3.13' cache: 'pip' cache-dependency-path: 'tests/requirements/py3.txt' - name: Install libmemcached-dev for pylibmc @@ -61,7 +61,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: '3.12' + python-version: '3.13' cache: 'pip' cache-dependency-path: 'tests/requirements/py3.txt' - name: Install libmemcached-dev for pylibmc diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index abe7c78a2554..5de554721d1f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -23,7 +23,7 @@ jobs: strategy: matrix: python-version: - - '3.12' + - '3.13' name: Windows, SQLite, Python ${{ matrix.python-version }} steps: - name: Checkout diff --git a/django/__init__.py b/django/__init__.py index e0b92dccfb39..e3f6bb49533d 100644 --- a/django/__init__.py +++ b/django/__init__.py @@ -1,6 +1,6 @@ from django.utils.version import get_version -VERSION = (5, 1, 2, "final", 0) +VERSION = (5, 1, 3, "final", 0) __version__ = get_version(VERSION) diff --git a/django/contrib/admin/templates/admin/includes/fieldset.html b/django/contrib/admin/templates/admin/includes/fieldset.html index a9d3f927025e..c94d46fd6478 100644 --- a/django/contrib/admin/templates/admin/includes/fieldset.html +++ b/django/contrib/admin/templates/admin/includes/fieldset.html @@ -1,8 +1,7 @@ -{% with name=fieldset.name|default:""|slugify %} -
- {% if name %} +
+ {% if fieldset.name %} {% if fieldset.is_collapsible %}
{% endif %} - {{ fieldset.name }} + {{ fieldset.name }} {% if fieldset.is_collapsible %}{% endif %} {% endif %} {% if fieldset.description %} @@ -36,6 +35,5 @@ {% if not line.fields|length == 1 %}{% endif %} {% endfor %} - {% if name and fieldset.is_collapsible %}
{% endif %} + {% if fieldset.name and fieldset.is_collapsible %}{% endif %}
-{% endwith %} diff --git a/django/contrib/gis/geoip2.py b/django/contrib/gis/geoip2.py index f5058c1c05dc..a5fe429b89ed 100644 --- a/django/contrib/gis/geoip2.py +++ b/django/contrib/gis/geoip2.py @@ -34,6 +34,18 @@ __all__ += ["GeoIP2", "GeoIP2Exception"] +# These are the values stored in the `database_type` field of the metadata. +# See https://maxmind.github.io/MaxMind-DB/#database_type for details. +SUPPORTED_DATABASE_TYPES = { + "DBIP-City-Lite", + "DBIP-Country-Lite", + "GeoIP2-City", + "GeoIP2-Country", + "GeoLite2-City", + "GeoLite2-Country", +} + + class GeoIP2Exception(Exception): pass @@ -106,7 +118,7 @@ def __init__(self, path=None, cache=0, country=None, city=None): ) database_type = self._metadata.database_type - if not database_type.endswith(("City", "Country")): + if database_type not in SUPPORTED_DATABASE_TYPES: raise GeoIP2Exception(f"Unable to handle database edition: {database_type}") def __del__(self): @@ -123,6 +135,14 @@ def __repr__(self): def _metadata(self): return self._reader.metadata() + @cached_property + def is_city(self): + return "City" in self._metadata.database_type + + @cached_property + def is_country(self): + return "Country" in self._metadata.database_type + def _query(self, query, *, require_city=False): if not isinstance(query, (str, ipaddress.IPv4Address, ipaddress.IPv6Address)): raise TypeError( @@ -130,9 +150,7 @@ def _query(self, query, *, require_city=False): "IPv6Address, not type %s" % type(query).__name__, ) - is_city = self._metadata.database_type.endswith("City") - - if require_city and not is_city: + if require_city and not self.is_city: raise GeoIP2Exception(f"Invalid GeoIP city data file: {self._path}") try: @@ -141,7 +159,7 @@ def _query(self, query, *, require_city=False): # GeoIP2 only takes IP addresses, so try to resolve a hostname. query = socket.gethostbyname(query) - function = self._reader.city if is_city else self._reader.country + function = self._reader.city if self.is_city else self._reader.country return function(query) def city(self, query): diff --git a/django/core/cache/backends/filebased.py b/django/core/cache/backends/filebased.py index 29d49c0ede9f..cbf47e4e1687 100644 --- a/django/core/cache/backends/filebased.py +++ b/django/core/cache/backends/filebased.py @@ -166,5 +166,5 @@ def _list_cache_files(self): """ return [ os.path.join(self._dir, fname) - for fname in glob.glob1(self._dir, "*%s" % self.cache_suffix) + for fname in glob.glob(f"*{self.cache_suffix}", root_dir=self._dir) ] diff --git a/django/core/validators.py b/django/core/validators.py index b1c5c053b846..8732ddf7adbf 100644 --- a/django/core/validators.py +++ b/django/core/validators.py @@ -101,13 +101,16 @@ def __init__(self, **kwargs): if self.accept_idna: self.regex = _lazy_re_compile( - self.hostname_re + self.domain_re + self.tld_re, re.IGNORECASE + r"^" + self.hostname_re + self.domain_re + self.tld_re + r"$", + re.IGNORECASE, ) else: self.regex = _lazy_re_compile( - self.ascii_only_hostname_re + r"^" + + self.ascii_only_hostname_re + self.ascii_only_domain_re - + self.ascii_only_tld_re, + + self.ascii_only_tld_re + + r"$", re.IGNORECASE, ) super().__init__(**kwargs) diff --git a/docs/conf.py b/docs/conf.py index 7b367e23e15e..047813814cc1 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -13,6 +13,8 @@ import sys from os.path import abspath, dirname, join +from sphinx import version_info as sphinx_version + # Workaround for sphinx-build recursion limit overflow: # pickle.dump(doctree, f, pickle.HIGHEST_PROTOCOL) # RuntimeError: maximum recursion depth exceeded while pickling an object @@ -138,13 +140,15 @@ def django_release(): extlinks = { "bpo": ("https://bugs.python.org/issue?@action=redirect&bpo=%s", "bpo-%s"), "commit": ("https://github.com/django/django/commit/%s", "%s"), - "cve": ("https://nvd.nist.gov/vuln/detail/CVE-%s", "CVE-%s"), "pypi": ("https://pypi.org/project/%s/", "%s"), # A file or directory. GitHub redirects from blob to tree if needed. "source": ("https://github.com/django/django/blob/main/%s", "%s"), "ticket": ("https://code.djangoproject.com/ticket/%s", "#%s"), } +if sphinx_version < (8, 1): + extlinks["cve"] = ("https://www.cve.org/CVERecord?id=CVE-%s", "CVE-%s") + # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # language = None diff --git a/docs/faq/install.txt b/docs/faq/install.txt index a89da571a96b..af1da879ec38 100644 --- a/docs/faq/install.txt +++ b/docs/faq/install.txt @@ -55,7 +55,7 @@ Django version Python versions 4.1 3.8, 3.9, 3.10, 3.11 (added in 4.1.3) 4.2 3.8, 3.9, 3.10, 3.11, 3.12 (added in 4.2.8) 5.0 3.10, 3.11, 3.12 -5.1 3.10, 3.11, 3.12 +5.1 3.10, 3.11, 3.12, 3.13 (added in 5.1.3) ============== =============== For each version of Python, only the latest micro release (A.B.C) is officially diff --git a/docs/howto/auth-remote-user.txt b/docs/howto/auth-remote-user.txt index 19b25432fe7c..f8492e367ad9 100644 --- a/docs/howto/auth-remote-user.txt +++ b/docs/howto/auth-remote-user.txt @@ -6,12 +6,11 @@ This document describes how to make use of external authentication sources (where the web server sets the ``REMOTE_USER`` environment variable) in your Django applications. This type of authentication solution is typically seen on intranet sites, with single sign-on solutions such as IIS and Integrated -Windows Authentication or Apache and `mod_authnz_ldap`_, `CAS`_, `Cosign`_, -`WebAuth`_, `mod_auth_sspi`_, etc. +Windows Authentication or Apache and `mod_authnz_ldap`_, `CAS`_, `WebAuth`_, +`mod_auth_sspi`_, etc. -.. _mod_authnz_ldap: https://httpd.apache.org/docs/2.2/mod/mod_authnz_ldap.html +.. _mod_authnz_ldap: https://httpd.apache.org/docs/current/mod/mod_authnz_ldap.html .. _CAS: https://www.apereo.org/projects/cas -.. _Cosign: http://weblogin.org .. _WebAuth: https://uit.stanford.edu/service/authentication .. _mod_auth_sspi: https://sourceforge.net/projects/mod-auth-sspi diff --git a/docs/howto/deployment/asgi/hypercorn.txt b/docs/howto/deployment/asgi/hypercorn.txt index ea5ce3cc72c6..3abd2d54efcf 100644 --- a/docs/howto/deployment/asgi/hypercorn.txt +++ b/docs/howto/deployment/asgi/hypercorn.txt @@ -17,7 +17,7 @@ You can install Hypercorn with ``pip``: Running Django in Hypercorn =========================== -When Hypercorn is installed, a ``hypercorn`` command is available +When :pypi:`Hypercorn` is installed, a ``hypercorn`` command is available which runs ASGI applications. Hypercorn needs to be called with the location of a module containing an ASGI application object, followed by what the application is called (separated by a colon). @@ -35,4 +35,4 @@ this command from the same directory as your ``manage.py`` file. For more advanced usage, please read the `Hypercorn documentation `_. -.. _Hypercorn: https://pgjones.gitlab.io/hypercorn/ +.. _Hypercorn: https://hypercorn.readthedocs.io/ diff --git a/docs/howto/index.txt b/docs/howto/index.txt index 0034032ce25e..d799ca79069d 100644 --- a/docs/howto/index.txt +++ b/docs/howto/index.txt @@ -1,38 +1,66 @@ -=============== -"How-to" guides -=============== +============= +How-to guides +============= -Here you'll find short answers to "How do I....?" types of questions. These -how-to guides don't cover topics in depth -- you'll find that material in the -:doc:`/topics/index` and the :doc:`/ref/index`. However, these guides will help -you quickly accomplish common tasks. +Practical guides covering common tasks and problems. + +Models, data and databases +========================== .. toctree:: :maxdepth: 1 - auth-remote-user - csrf - custom-management-commands - custom-model-fields - custom-lookups - custom-template-backend - custom-template-tags - custom-file-storage - deployment/index - upgrade-version - error-reporting initial-data legacy-databases - logging + custom-model-fields + writing-migrations + custom-lookups + +Templates and output +==================== + +.. toctree:: + :maxdepth: 1 + outputting-csv outputting-pdf overriding-templates + custom-template-backend + custom-template-tags + +Project configuration and management +==================================== + +.. toctree:: + :maxdepth: 1 + static-files/index - static-files/deployment - windows - writing-migrations + logging + error-reporting delete-app +Installing, deploying and upgrading +=================================== + +.. toctree:: + :maxdepth: 1 + + upgrade-version + windows + deployment/index + static-files/deployment + +Other guides +============ + +.. toctree:: + :maxdepth: 1 + + auth-remote-user + csrf + custom-management-commands + custom-file-storage + .. seealso:: The `Django community aggregator`_, where we aggregate content from the diff --git a/docs/howto/overriding-templates.txt b/docs/howto/overriding-templates.txt index f636948a201d..f99a1203a8d6 100644 --- a/docs/howto/overriding-templates.txt +++ b/docs/howto/overriding-templates.txt @@ -111,15 +111,15 @@ reimplement the entire template. For example, you can use this technique to add a custom logo to the ``admin/base_site.html`` template: - .. code-block:: html+django - :caption: ``templates/admin/base_site.html`` +.. code-block:: html+django + :caption: ``templates/admin/base_site.html`` - {% extends "admin/base_site.html" %} + {% extends "admin/base_site.html" %} - {% block branding %} - logo - {{ block.super }} - {% endblock %} + {% block branding %} + logo + {{ block.super }} + {% endblock %} Key points to note: diff --git a/docs/howto/windows.txt b/docs/howto/windows.txt index 83aa8d065573..235b18a24ff3 100644 --- a/docs/howto/windows.txt +++ b/docs/howto/windows.txt @@ -2,7 +2,7 @@ How to install Django on Windows ================================ -This document will guide you through installing Python 3.12 and Django on +This document will guide you through installing Python 3.13 and Django on Windows. It also provides instructions for setting up a virtual environment, which makes it easier to work on Python projects. This is meant as a beginner's guide for users working on Django projects and does not reflect how Django @@ -18,7 +18,7 @@ Install Python ============== Django is a Python web framework, thus requiring Python to be installed on your -machine. At the time of writing, Python 3.12 is the latest version. +machine. At the time of writing, Python 3.13 is the latest version. To install Python on your machine go to https://www.python.org/downloads/. The website should offer you a download button for the latest Python version. diff --git a/docs/internals/_images/triage_process.svg b/docs/internals/_images/triage_process.svg index 2b5e0d3cedd3..6fbf1cbcc7f2 100644 --- a/docs/internals/_images/triage_process.svg +++ b/docs/internals/_images/triage_process.svg @@ -232,47 +232,47 @@ - - - The ticket was already reported, was - already rejected, isn't a bug, doesn't contain - enough information, or can't be reproduced. + + + The ticket was already reported, was + already rejected, isn't a bug, doesn't contain + enough information, or can't be reproduced. - + - + - - - The ticket is a - bug and should - be fixed. + + + The ticket is a + bug and should + be fixed. - + - + - - - The ticket has a patch which applies cleanly and includes all - needed tests and docs. A merger can commit it as is. + + + The ticket has a patch which applies cleanly and includes all + needed tests and docs. A merger can commit it as is. - + - + diff --git a/docs/internals/contributing/triaging-tickets.txt b/docs/internals/contributing/triaging-tickets.txt index 852219c96caa..7987d63e9a61 100644 --- a/docs/internals/contributing/triaging-tickets.txt +++ b/docs/internals/contributing/triaging-tickets.txt @@ -49,8 +49,8 @@ attribute easily tells us what and who each ticket is waiting on. Since a picture is worth a thousand words, let's start there: .. image:: /internals/_images/triage_process.* - :height: 501 - :width: 400 + :height: 750 + :width: 600 :alt: Django's ticket triage workflow We've got two roles in this diagram: diff --git a/docs/internals/contributing/writing-code/submitting-patches.txt b/docs/internals/contributing/writing-code/submitting-patches.txt index c63cc6f9f02c..a002051bbb63 100644 --- a/docs/internals/contributing/writing-code/submitting-patches.txt +++ b/docs/internals/contributing/writing-code/submitting-patches.txt @@ -111,7 +111,7 @@ requirements: feature, the change should also contain documentation. When you think your work is ready to be reviewed, send :doc:`a GitHub pull -request `. +request `. If you can't send a pull request for some reason, you can also use patches in Trac. When using this style, follow these guidelines. @@ -137,20 +137,63 @@ Regardless of the way you submit your work, follow these steps. .. _ticket tracker: https://code.djangoproject.com/ .. _Development dashboard: https://dashboard.djangoproject.com/ -Non-trivial contributions -========================= +Contributions which require community feedback +============================================== -A "non-trivial" contribution is one that is more than a small bug fix. It's a -change that introduces new Django functionality and makes some sort of design -decision. +A wider community discussion is required when a patch introduces new Django +functionality and makes some sort of design decision. This is especially +important if the approach involves a :ref:`deprecation ` +or introduces breaking changes. -If you provide a non-trivial change, include evidence that alternatives have -been discussed on the `Django Forum`_ or |django-developers| list. +The following are different approaches for gaining feedback from the community. -If you're not sure whether your contribution should be considered non-trivial, -ask on the ticket for opinions. +The Django Forum or django-developers mailing list +-------------------------------------------------- + +You can propose a change on the `Django Forum`_ or |django-developers| mailing +list. You should explain the need for the change, go into details of the +approach and discuss alternatives. + +Please include a link to such discussions in your contributions. + +Third party package +------------------- + +Django does not accept experimental features. All features must follow our +:ref:`deprecation policy `. Hence, it can +take months or years for Django to iterate on an API design. + +If you need user feedback on a public interface, it is better to create a +third-party package first. You can iterate on the public API much faster, while +also validating the need for the feature. + +Once this package becomes stable and there are clear benefits of incorporating +aspects into Django core, starting a discussion on the `Django Forum`_ or +|django-developers| mailing list would be the next step. + +Django Enhancement Proposal (DEP) +--------------------------------- + +Similar to Python’s PEPs, Django has `Django Enhancement Proposals`_ or DEPs. A +DEP is a design document which provides information to the Django community, or +describes a new feature or process for Django. They provide concise technical +specifications of features, along with rationales. DEPs are also the primary +mechanism for proposing and collecting community input on major new features. + +Before considering writing a DEP, it is recommended to first open a discussion +on the `Django Forum`_ or |django-developers| mailing list. This allows the +community to provide feedback and helps refine the proposal. 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: + +* `DEP 181: ORM Expressions `_ +* `DEP 182: Multiple Template Engines `_ +* `DEP 201: Simplified routing syntax `_ .. _Django Forum: https://forum.djangoproject.com/ +.. _Django Enhancement Proposals: https://github.com/django/deps .. _deprecating-a-feature: diff --git a/docs/internals/security.txt b/docs/internals/security.txt index 55300b01e170..6aac9a6b660d 100644 --- a/docs/internals/security.txt +++ b/docs/internals/security.txt @@ -38,6 +38,41 @@ action to be taken, you may receive further followup emails. .. _our public Trac instance: https://code.djangoproject.com/query +.. _security-report-evaluation: + +How does Django evaluate a report +================================= + +These are criteria used by the security team when evaluating whether a report +requires a security release: + +* The vulnerability is within a :ref:`supported version ` of + Django. + +* The vulnerability applies to a production-grade Django application. This means + the following do not require a security release: + + * Exploits that only affect local development, for example when using + :djadmin:`runserver`. + * Exploits which fail to follow security best practices, such as failure to + sanitize user input. For other examples, see our :ref:`security + documentation `. + * Exploits in AI generated code that do not adhere to security best practices. + +The security team may conclude that the source of the vulnerability is within +the Python standard library, in which case the reporter will be asked to report +the vulnerability to the Python core team. For further details see the `Python +security guidelines `_. + +On occasion, a security release may be issued to help resolve a security +vulnerability within a popular third-party package. These reports should come +from the package maintainers. + +If you are unsure whether your finding meets these criteria, please still report +it :ref:`privately by emailing security@djangoproject.com +`. The security team will review your report and +recommend the correct course of action. + .. _security-support: Supported versions diff --git a/docs/intro/contributing.txt b/docs/intro/contributing.txt index 7d590e76a21b..0900fdae37e4 100644 --- a/docs/intro/contributing.txt +++ b/docs/intro/contributing.txt @@ -217,8 +217,7 @@ a dependency for one or more of the Python packages. Consult the failing package's documentation or search the web with the error message that you encounter. -Now we are ready to run the test suite. If you're using GNU/Linux, macOS, or -some other flavor of Unix, run: +Now we are ready to run the test suite: .. console:: diff --git a/docs/intro/overview.txt b/docs/intro/overview.txt index 8314b3d35159..af87a01bb478 100644 --- a/docs/intro/overview.txt +++ b/docs/intro/overview.txt @@ -26,7 +26,7 @@ representing your models -- so far, it's been solving many years' worth of database-schema problems. Here's a quick example: .. code-block:: python - :caption: ``mysite/news/models.py`` + :caption: ``news/models.py`` from django.db import models @@ -151,7 +151,7 @@ a website that lets authenticated users add, change and delete objects. The only step required is to register your model in the admin site: .. code-block:: python - :caption: ``mysite/news/models.py`` + :caption: ``news/models.py`` from django.db import models @@ -163,7 +163,7 @@ only step required is to register your model in the admin site: reporter = models.ForeignKey(Reporter, on_delete=models.CASCADE) .. code-block:: python - :caption: ``mysite/news/admin.py`` + :caption: ``news/admin.py`` from django.contrib import admin @@ -195,7 +195,7 @@ Here's what a URLconf might look like for the ``Reporter``/``Article`` example above: .. code-block:: python - :caption: ``mysite/news/urls.py`` + :caption: ``news/urls.py`` from django.urls import path @@ -235,7 +235,7 @@ and renders the template with the retrieved data. Here's an example view for ``year_archive`` from above: .. code-block:: python - :caption: ``mysite/news/views.py`` + :caption: ``news/views.py`` from django.shortcuts import render @@ -265,7 +265,7 @@ Let's say the ``news/year_archive.html`` template was found. Here's what that might look like: .. code-block:: html+django - :caption: ``mysite/news/templates/news/year_archive.html`` + :caption: ``news/templates/news/year_archive.html`` {% extends "base.html" %} @@ -306,10 +306,10 @@ Here's what the "base.html" template, including the use of :doc:`static files `, might look like: .. code-block:: html+django - :caption: ``mysite/templates/base.html`` + :caption: ``templates/base.html`` {% load static %} - + {% block title %}{% endblock %} diff --git a/docs/intro/reusable-apps.txt b/docs/intro/reusable-apps.txt index 98f21c9d91b2..5acf8c2b182a 100644 --- a/docs/intro/reusable-apps.txt +++ b/docs/intro/reusable-apps.txt @@ -6,7 +6,7 @@ This advanced tutorial begins where :doc:`Tutorial 8 ` left off. We'll be turning our web-poll into a standalone Python package you can reuse in new projects and share with other people. -If you haven't recently completed Tutorials 1–7, we encourage you to review +If you haven't recently completed Tutorials 1–8, we encourage you to review these so that your example project matches the one described below. Reusability matters @@ -57,7 +57,7 @@ After the previous tutorials, our project should look like this: .. code-block:: text - mysite/ + djangotutorial/ manage.py mysite/ __init__.py @@ -90,12 +90,12 @@ After the previous tutorials, our project should look like this: admin/ base_site.html -You created ``mysite/templates`` in :doc:`Tutorial 7 `, -and ``polls/templates`` in :doc:`Tutorial 3 `. Now perhaps -it is clearer why we chose to have separate template directories for the -project and application: everything that is part of the polls application is in -``polls``. It makes the application self-contained and easier to drop into a -new project. +You created ``djangotutorial/templates`` in :doc:`Tutorial 7 +`, and ``polls/templates`` in +:doc:`Tutorial 3 `. Now perhaps it is clearer why we chose +to have separate template directories for the project and application: +everything that is part of the polls application is in ``polls``. It makes the +application self-contained and easier to drop into a new project. The ``polls`` directory could now be copied into a new Django project and immediately reused. It's not quite ready to be published though. For that, we @@ -237,6 +237,7 @@ this. For a small app like polls, this process isn't too difficult. "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", ] diff --git a/docs/intro/tutorial01.txt b/docs/intro/tutorial01.txt index 0af25d209b00..b840eb266025 100644 --- a/docs/intro/tutorial01.txt +++ b/docs/intro/tutorial01.txt @@ -48,14 +48,21 @@ including database configuration, Django-specific options and application-specific settings. From the command line, ``cd`` into a directory where you'd like to store your -code, then run the following command: +code and create a new directory named ``djangotutorial``. (This directory name +doesn't matter to Django; you can rename it to anything you like.) .. console:: - $ django-admin startproject mysite + $ mkdir djangotutorial -This will create a ``mysite`` directory in your current directory. If it didn't -work, see :ref:`troubleshooting-django-admin`. +Then, run the following command to bootstrap a new Django project: + +.. console:: + + $ django-admin startproject mysite djangotutorial + +This will create a project called ``mysite`` inside the ``djangotutorial`` +directory. If it didn't work, see :ref:`troubleshooting-django-admin`. .. note:: @@ -68,7 +75,7 @@ Let's look at what :djadmin:`startproject` created: .. code-block:: text - mysite/ + djangotutorial/ manage.py mysite/ __init__.py @@ -79,14 +86,11 @@ Let's look at what :djadmin:`startproject` created: These files are: -* The outer :file:`mysite/` root directory is a container for your project. Its - name doesn't matter to Django; you can rename it to anything you like. - * :file:`manage.py`: A command-line utility that lets you interact with this Django project in various ways. You can read all the details about :file:`manage.py` in :doc:`/ref/django-admin`. -* The inner :file:`mysite/` directory is the actual Python package for your +* :file:`mysite/`: A directory that is the actual Python package for your project. Its name is the Python package name you'll need to use to import anything inside it (e.g. ``mysite.urls``). @@ -111,8 +115,8 @@ These files are: The development server ====================== -Let's verify your Django project works. Change into the outer :file:`mysite` directory, if -you haven't already, and run the following commands: +Let's verify your Django project works. Change into the :file:`djangotutorial` +directory, if you haven't already, and run the following commands: .. console:: @@ -179,10 +183,8 @@ rather than creating directories. configuration and apps for a particular website. A project can contain multiple apps. An app can be in multiple projects. -Your apps can live anywhere on your :ref:`Python path `. In -this tutorial, we'll create our poll app in the same directory as your -:file:`manage.py` file so that it can be imported as its own top-level module, -rather than a submodule of ``mysite``. +Your apps can live anywhere in your :ref:`Python path `. In +this tutorial, we'll create our poll app inside the ``djangotutorial`` folder. To create your app, make sure you're in the same directory as :file:`manage.py` and type this command: @@ -288,7 +290,8 @@ app will still work. .. admonition:: When to use :func:`~django.urls.include()` You should always use ``include()`` when you include other URL patterns. - ``admin.site.urls`` is the only exception to this. + The only exception is ``admin.site.urls``, which is a pre-built URLconf + provided by Django for the default admin site. You have now wired an ``index`` view into the URLconf. Verify it's working with the following command: diff --git a/docs/intro/tutorial05.txt b/docs/intro/tutorial05.txt index 5f501ce92f5b..28a634b8c381 100644 --- a/docs/intro/tutorial05.txt +++ b/docs/intro/tutorial05.txt @@ -216,7 +216,7 @@ and you'll see something like: FAIL: test_was_published_recently_with_future_question (polls.tests.QuestionModelTests) ---------------------------------------------------------------------- Traceback (most recent call last): - File "/path/to/mysite/polls/tests.py", line 16, in test_was_published_recently_with_future_question + File "/path/to/djangotutorial/polls/tests.py", line 16, in test_was_published_recently_with_future_question self.assertIs(future_question.was_published_recently(), False) AssertionError: True is not False diff --git a/docs/intro/tutorial07.txt b/docs/intro/tutorial07.txt index e0c87be8987c..62eded486488 100644 --- a/docs/intro/tutorial07.txt +++ b/docs/intro/tutorial07.txt @@ -306,10 +306,10 @@ powered by Django itself, and its interfaces use Django's own template system. Customizing your *project's* templates -------------------------------------- -Create a ``templates`` directory in your project directory (the one that -contains ``manage.py``). Templates can live anywhere on your filesystem that -Django can access. (Django runs as whatever user your server runs.) However, -keeping your templates within the project is a good convention to follow. +Create a ``templates`` directory in your ``djangotutorial`` directory. +Templates can live anywhere on your filesystem that Django can access. (Django +runs as whatever user your server runs.) However, keeping your templates within +the project is a good convention to follow. Open your settings file (:file:`mysite/settings.py`, remember) and add a :setting:`DIRS ` option in the :setting:`TEMPLATES` setting: diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index c2061f11ab79..4a8b52c18f66 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/docs/ref/contrib/admin/index.txt @@ -1473,6 +1473,27 @@ templates used by the :class:`ModelAdmin` views: See also :ref:`saving-objects-in-the-formset`. +.. warning:: + + All hooks that return a ``ModelAdmin`` property return the property itself + rather than a copy of its value. Dynamically modifying the value can lead + to surprising results. + + Let's take :meth:`ModelAdmin.get_readonly_fields` as an example:: + + class PersonAdmin(admin.ModelAdmin): + readonly_fields = ["name"] + + def get_readonly_fields(self, request, obj=None): + readonly = super().get_readonly_fields(request, obj) + if not request.user.is_superuser: + readonly.append("age") # Edits the class attribute. + return readonly + + This results in ``readonly_fields`` becoming + ``["name", "age", "age", ...]``, even for a superuser, as ``"age"`` is added + each time non-superuser visits the page. + .. method:: ModelAdmin.get_ordering(request) The ``get_ordering`` method takes a ``request`` as parameter and diff --git a/docs/ref/contrib/auth.txt b/docs/ref/contrib/auth.txt index 036f8d9f7678..9185c9d0f94a 100644 --- a/docs/ref/contrib/auth.txt +++ b/docs/ref/contrib/auth.txt @@ -54,7 +54,8 @@ Fields Required. A hash of, and metadata about, the password. (Django doesn't store the raw password.) Raw passwords can be arbitrarily long and can - contain any character. See the :doc:`password documentation + contain any character. The metadata in this field may mark the password + as unusable. See the :doc:`password documentation `. .. attribute:: groups @@ -179,8 +180,9 @@ Methods .. method:: set_unusable_password() - Marks the user as having no password set. This isn't the same as - having a blank string for a password. + Marks the user as having no password set by updating the metadata in + the :attr:`~django.contrib.auth.models.User.password` field. This isn't + the same as having a blank string for a password. :meth:`~django.contrib.auth.models.User.check_password()` for this user will never return ``True``. Doesn't save the :class:`~django.contrib.auth.models.User` object. diff --git a/docs/ref/contrib/flatpages.txt b/docs/ref/contrib/flatpages.txt index c82fb5de85c0..01e5553ff3cd 100644 --- a/docs/ref/contrib/flatpages.txt +++ b/docs/ref/contrib/flatpages.txt @@ -256,7 +256,7 @@ Here's a sample :file:`flatpages/default.html` template: .. code-block:: html+django - + {{ flatpage.title }} diff --git a/docs/ref/files/storage.txt b/docs/ref/files/storage.txt index f7c290a15098..52c8f90427d5 100644 --- a/docs/ref/files/storage.txt +++ b/docs/ref/files/storage.txt @@ -11,7 +11,25 @@ Django provides convenient ways to access the default storage class: .. data:: storages - Storage instances as defined by :setting:`STORAGES`. + A dictionary-like object that allows retrieving a storage instance using + its alias as defined by :setting:`STORAGES`. + + ``storages`` has an attribute ``backends``, which defaults to the raw value + provided in :setting:`STORAGES`. + + Additionally, ``storages`` provides a ``create_storage()`` method that + accepts the dictionary used in :setting:`STORAGES` for a backend, and + returns a storage instance based on that backend definition. This may be + useful for third-party packages needing to instantiate storages in tests: + + .. code-block:: pycon + + >>> from django.core.files.storage import storages + >>> storages.backends + {'default': {'BACKEND': 'django.core.files.storage.FileSystemStorage'}, + 'staticfiles': {'BACKEND': 'django.contrib.staticfiles.storage.StaticFilesStorage'}, + 'custom': {'BACKEND': 'package.storage.CustomStorage'}} + >>> storage_instance = storages.create_storage({"BACKEND": "package.storage.CustomStorage"}) .. class:: DefaultStorage diff --git a/docs/ref/forms/api.txt b/docs/ref/forms/api.txt index 28cd452c4e8b..2315757eb75e 100644 --- a/docs/ref/forms/api.txt +++ b/docs/ref/forms/api.txt @@ -406,8 +406,8 @@ process: .. code-block:: pycon >>> f.base_fields["subject"].label_suffix = "?" - >>> another_f = CommentForm(auto_id=False) - >>> f.as_div().split("")[0] + >>> another_f = ContactForm(auto_id=False) + >>> another_f.as_div().split("")[0] '
' Accessing "clean" data @@ -770,7 +770,7 @@ The template used by ``as_table()``. Default: ``'django/forms/table.html'``. >>> f = ContactForm() >>> f.as_table() '\n\n\n' - >>> print(f) + >>> print(f.as_table()) diff --git a/docs/ref/forms/fields.txt b/docs/ref/forms/fields.txt index 3ee33c966109..506947c31d7b 100644 --- a/docs/ref/forms/fields.txt +++ b/docs/ref/forms/fields.txt @@ -112,7 +112,7 @@ validation may not be correct when adding and deleting formsets. The ``label`` argument lets you specify the "human-friendly" label for this field. This is used when the ``Field`` is displayed in a ``Form``. -As explained in "Outputting forms as HTML" above, the default label for a +As explained in :ref:`ref-forms-api-outputting-html`, the default label for a ``Field`` is generated from the field name by converting all underscores to spaces and upper-casing the first letter. Specify ``label`` if that default behavior doesn't result in an adequate label. @@ -226,7 +226,7 @@ validation if a particular field's value is not given. ``initial`` values are >>> f = CommentForm(data) >>> f.is_valid() False - # The form does *not* fall back to using the initial values. + # The form does *not* fallback to using the initial values. >>> f.errors {'url': ['This field is required.'], 'name': ['This field is required.']} @@ -379,7 +379,7 @@ See the :doc:`validators documentation ` for more information. The ``localize`` argument enables the localization of form data input, as well as the rendered output. -See the :doc:`format localization ` documentation for +See the :doc:`format localization documentation ` for more information. ``disabled`` diff --git a/docs/ref/models/fields.txt b/docs/ref/models/fields.txt index 069ecdbabbd0..2b1ce96dda5c 100644 --- a/docs/ref/models/fields.txt +++ b/docs/ref/models/fields.txt @@ -22,9 +22,9 @@ This document contains all the API references of :class:`Field` including the .. note:: - Technically, these models are defined in :mod:`django.db.models.fields`, but - for convenience they're imported into :mod:`django.db.models`; the standard - convention is to use ``from django.db import models`` and refer to fields as + Fields are defined in :mod:`django.db.models.fields`, but for convenience + they're imported into :mod:`django.db.models`. The standard convention is + to use ``from django.db import models`` and refer to fields as ``models.Field``. .. _common-model-field-options: @@ -434,6 +434,11 @@ precedence when creating instances in Python code. ``db_default`` will still be set at the database level and will be used when inserting rows outside of the ORM or when adding a new field in a migration. +If a field has a ``db_default`` without a ``default`` set and no value is +assigned to the field, a ``DatabaseDefault`` object is returned as the field +value on unsaved model instances. The actual value for the field is determined +by the database when the model instance is saved. + ``db_index`` ------------ @@ -1634,80 +1639,25 @@ Django also defines a set of fields that represent relations. .. class:: ForeignKey(to, on_delete, **options) A many-to-one relationship. Requires two positional arguments: the class to -which the model is related and the :attr:`~ForeignKey.on_delete` option. - -.. _recursive-relationships: - -To create a recursive relationship -- an object that has a many-to-one -relationship with itself -- use ``models.ForeignKey('self', -on_delete=models.CASCADE)``. - -.. _lazy-relationships: - -If you need to create a relationship on a model that has not yet been defined, -you can use the name of the model, rather than the model object itself:: +which the model is related and the :attr:`~ForeignKey.on_delete` option:: from django.db import models - class Car(models.Model): - manufacturer = models.ForeignKey( - "Manufacturer", - on_delete=models.CASCADE, - ) - # ... - - class Manufacturer(models.Model): - # ... - pass - -Relationships defined this way on :ref:`abstract models -` are resolved when the model is subclassed as a -concrete model and are not relative to the abstract model's ``app_label``: - -.. code-block:: python - :caption: ``products/models.py`` - - from django.db import models + name = models.TextField() - class AbstractCar(models.Model): - manufacturer = models.ForeignKey("Manufacturer", on_delete=models.CASCADE) - - class Meta: - abstract = True - -.. code-block:: python - :caption: ``production/models.py`` - - from django.db import models - from products.models import AbstractCar - - - class Manufacturer(models.Model): - pass - - - class Car(AbstractCar): - pass - - - # Car.manufacturer will point to `production.Manufacturer` here. - -To refer to models defined in another application, you can explicitly specify -a model with the full application label. For example, if the ``Manufacturer`` -model above is defined in another application called ``production``, you'd -need to use:: - class Car(models.Model): - manufacturer = models.ForeignKey( - "production.Manufacturer", - on_delete=models.CASCADE, - ) + manufacturer = models.ForeignKey(Manufacturer, on_delete=models.CASCADE) -This sort of reference, called a lazy relationship, can be useful when -resolving circular import dependencies between two applications. +The first positional argument can be either a concrete model class or a +:ref:`lazy reference ` to a model class. +:ref:`Recursive relationships `, where a model has a +relationship with itself, are also supported. + +See :attr:`ForeignKey.on_delete` for details on the second positional +argument. A database index is automatically created on the ``ForeignKey``. You can disable this by setting :attr:`~Field.db_index` to ``False``. You may want to @@ -1720,9 +1670,9 @@ Database Representation Behind the scenes, Django appends ``"_id"`` to the field name to create its database column name. In the above example, the database table for the ``Car`` -model will have a ``manufacturer_id`` column. (You can change this explicitly by -specifying :attr:`~Field.db_column`) However, your code should never have to -deal with the database column name, unless you write custom SQL. You'll always +model will have a ``manufacturer_id`` column. You can change this explicitly by +specifying :attr:`~Field.db_column`, however, your code should never have to +deal with the database column name (unless you write custom SQL). You'll always deal with the field names of your model object. .. _foreign-key-arguments: @@ -2272,6 +2222,120 @@ accepted by :class:`ForeignKey`, plus one extra argument: See :doc:`One-to-one relationships ` for usage examples of ``OneToOneField``. +.. _lazy-relationships: + +Lazy relationships +------------------ + +Lazy relationships allow referencing models by their names (as strings) or +creating recursive relationships. Strings can be used as the first argument in +any relationship field to reference models lazily. A lazy reference can be +either :ref:`recursive `, +:ref:`relative ` or +:ref:`absolute `. + +.. _recursive-relationships: + +Recursive +~~~~~~~~~ + +To define a relationship where a model references itself, use ``"self"`` as the +first argument of the relationship field:: + + from django.db import models + + + class Manufacturer(models.Model): + name = models.TextField() + suppliers = models.ManyToManyField("self", symmetrical=False) + + +When used in an :ref:`abstract model `, the recursive +relationship resolves such that each concrete subclass references itself. + +.. _relative-relationships: + +Relative +~~~~~~~~ + +When a relationship needs to be created with a model that has not been defined +yet, it can be referenced by its name rather than the model object itself:: + + from django.db import models + + + class Car(models.Model): + manufacturer = models.ForeignKey( + "Manufacturer", + on_delete=models.CASCADE, + ) + + + class Manufacturer(models.Model): + name = models.TextField() + suppliers = models.ManyToManyField("self", symmetrical=False) + +Relationships defined this way on :ref:`abstract models +` are resolved when the model is subclassed as a +concrete model and are not relative to the abstract model's ``app_label``: + +.. code-block:: python + :caption: ``products/models.py`` + + from django.db import models + + + class AbstractCar(models.Model): + manufacturer = models.ForeignKey("Manufacturer", on_delete=models.CASCADE) + + class Meta: + abstract = True + +.. code-block:: python + :caption: ``production/models.py`` + + from django.db import models + from products.models import AbstractCar + + + class Manufacturer(models.Model): + name = models.TextField() + + + class Car(AbstractCar): + pass + +In this example, the ``Car.manufacturer`` relationship will resolve to +``production.Manufacturer``, as it points to the concrete model defined +within the ``production/models.py`` file. + +.. admonition:: Reusable models with relative references + + Relative references allow the creation of reusable abstract models with + relationships that can resolve to different implementations of the + referenced models in various subclasses across different applications. + +.. _absolute-relationships: + +Absolute +~~~~~~~~ + +Absolute references specify a model using its ``app_label`` and class name, +allowing for model references across different applications. This type of lazy +relationship can also help resolve circular imports. + +For example, if the ``Manufacturer`` model is defined in another application +called ``thirdpartyapp``, it can be referenced as:: + + class Car(models.Model): + manufacturer = models.ForeignKey( + "thirdpartyapp.Manufacturer", + on_delete=models.CASCADE, + ) + +Absolute references always point to the same model, even when used in an +:ref:`abstract model `. + Field API reference =================== diff --git a/docs/ref/templates/builtins.txt b/docs/ref/templates/builtins.txt index 86841b3dbd38..c00baf3a0779 100644 --- a/docs/ref/templates/builtins.txt +++ b/docs/ref/templates/builtins.txt @@ -3110,7 +3110,7 @@ slightly different call: {% load static %} {% static "images/hi.jpg" as myphoto %} - + Hi! .. admonition:: Using Jinja2 templates? diff --git a/docs/releases/5.1.3.txt b/docs/releases/5.1.3.txt new file mode 100644 index 000000000000..9e251f221f7c --- /dev/null +++ b/docs/releases/5.1.3.txt @@ -0,0 +1,22 @@ +========================== +Django 5.1.3 release notes +========================== + +*November 5, 2024* + +Django 5.1.3 fixes several bugs in 5.1.2 and adds compatibility with Python +3.13. + +Bugfixes +======== + +* Fixed a bug in Django 5.1 where + :class:`~django.core.validators.DomainNameValidator` accepted any input value + that contained a valid domain name, rather than only input values that were a + valid domain name (:ticket:`35845`). + +* Fixed a regression in Django 5.1 that prevented the use of DB-IP databases + with :class:`~django.contrib.gis.geoip2.GeoIP2` (:ticket:`35841`). + +* Fixed a regression in Django 5.1 where non-ASCII fieldset names were not + displayed when rendering admin fieldsets (:ticket:`35876`). diff --git a/docs/releases/5.1.txt b/docs/releases/5.1.txt index bc868fddda6e..037c76fd5453 100644 --- a/docs/releases/5.1.txt +++ b/docs/releases/5.1.txt @@ -18,8 +18,9 @@ project. Python compatibility ==================== -Django 5.1 supports Python 3.10, 3.11, and 3.12. We **highly recommend** and -only officially support the latest release of each series. +Django 5.1 supports Python 3.10, 3.11, 3.12, and 3.13 (as of 5.1.3). We +**highly recommend** and only officially support the latest release of each +series. .. _whats-new-5.1: diff --git a/docs/releases/index.txt b/docs/releases/index.txt index 7db747c97f45..06c1461ab7b2 100644 --- a/docs/releases/index.txt +++ b/docs/releases/index.txt @@ -25,6 +25,7 @@ versions of the documentation contain the release notes for any later releases. .. toctree:: :maxdepth: 1 + 5.1.3 5.1.2 5.1.1 5.1 diff --git a/docs/spelling_wordlist b/docs/spelling_wordlist index d715e62e054b..747a712a62eb 100644 --- a/docs/spelling_wordlist +++ b/docs/spelling_wordlist @@ -123,6 +123,7 @@ deduplicates deduplication deepcopy deferrable +DEP deprecations deserialization deserialize @@ -141,6 +142,7 @@ Disqus distro django djangoproject +djangotutorial dm docstring docstrings diff --git a/docs/topics/db/models.txt b/docs/topics/db/models.txt index ee4bdab7bb38..62488a75f791 100644 --- a/docs/topics/db/models.txt +++ b/docs/topics/db/models.txt @@ -228,6 +228,15 @@ ones: object. If callable it will be called every time a new object is created. +:attr:`~Field.db_default` + The database-computed default value for the field. This can be a literal + value or a database function. + + If both ``db_default`` and :attr:`Field.default` are set, ``default`` will + take precedence when creating instances in Python code. ``db_default`` will + still be set at the database level and will be used when inserting rows + outside of the ORM or when adding a new field in a migration. + :attr:`~Field.help_text` Extra "help" text to be displayed with the form widget. It's useful for documentation even if your field isn't used on a form. diff --git a/docs/topics/db/queries.txt b/docs/topics/db/queries.txt index 45c6183103c3..7e3338eaea33 100644 --- a/docs/topics/db/queries.txt +++ b/docs/topics/db/queries.txt @@ -62,7 +62,8 @@ class represents a particular record in the database table. To create an object, instantiate it using keyword arguments to the model class, then call :meth:`~django.db.models.Model.save` to save it to the database. -Assuming models live in a file ``mysite/blog/models.py``, here's an example: +Assuming models live in a ``models.py`` file inside a ``blog`` Django app, here +is an example: .. code-block:: pycon diff --git a/docs/topics/email.txt b/docs/topics/email.txt index 75a50f40a142..109efe8ad364 100644 --- a/docs/topics/email.txt +++ b/docs/topics/email.txt @@ -311,9 +311,11 @@ All parameters are optional and can be set at any time prior to calling the * ``bcc``: A list or tuple of addresses used in the "Bcc" header when sending the email. -* ``connection``: An email backend instance. Use this parameter if - you want to use the same connection for multiple messages. If omitted, a - new connection is created when ``send()`` is called. +* ``connection``: An :ref:`email backend ` instance. Use + this parameter if you are sending the ``EmailMessage`` via ``send()`` and you + want to use the same connection for multiple messages. If omitted, a new + connection is created when ``send()`` is called. This parameter is ignored + when using :ref:`send_messages() `. * ``attachments``: A list of attachments to put on the message. These can be either :class:`~email.mime.base.MIMEBase` instances, or ``(filename, @@ -662,9 +664,10 @@ destroying a connection every time you want to send an email. There are two ways you tell an email backend to reuse a connection. -Firstly, you can use the ``send_messages()`` method. ``send_messages()`` takes -a list of :class:`~django.core.mail.EmailMessage` instances (or subclasses), -and sends them all using a single connection. +Firstly, you can use the ``send_messages()`` method on a connection. This takes +a list of :class:`EmailMessage` (or subclass) instances, and sends them all +using that single connection. As a consequence, any :class:`connection +` set on an individual message is ignored. For example, if you have a function called ``get_notification_email()`` that returns a list of :class:`~django.core.mail.EmailMessage` objects representing diff --git a/docs/topics/http/views.txt b/docs/topics/http/views.txt index 2985bfb72b2c..feb4eaa4ecb7 100644 --- a/docs/topics/http/views.txt +++ b/docs/topics/http/views.txt @@ -23,7 +23,7 @@ Here's a view that returns the current date and time, as an HTML document:: def current_datetime(request): now = datetime.datetime.now() - html = "It is now %s." % now + html = 'It is now %s.' % now return HttpResponse(html) Let's step through this code one line at a time: @@ -225,7 +225,7 @@ Here's an example of an async view:: async def current_datetime(request): now = datetime.datetime.now() - html = "It is now %s." % now + html = 'It is now %s.' % now return HttpResponse(html) You can read more about Django's async support, and how to best use async diff --git a/pyproject.toml b/pyproject.toml index 5c85cf42b618..19bc17ba1a6e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,7 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Internet :: WWW/HTTP :: WSGI", diff --git a/tests/admin_inlines/tests.py b/tests/admin_inlines/tests.py index 2c148a49f0ec..4959afb02d3f 100644 --- a/tests/admin_inlines/tests.py +++ b/tests/admin_inlines/tests.py @@ -1785,7 +1785,7 @@ def test_inline_headings(self): # The second and third have the same "Advanced options" name, but the # second one has the "collapse" class. for x, classes in ((1, ""), (2, "collapse")): - heading_id = f"fieldset-0-advanced-options-{x}-heading" + heading_id = f"fieldset-0-{x}-heading" with self.subTest(heading_id=heading_id): self.assertContains( response, @@ -1830,7 +1830,7 @@ def test_inline_headings(self): # Every fieldset defined for an inline's form. for z, fieldset in enumerate(inline_admin_form): if fieldset.name: - heading_id = f"{prefix}-{y}-details-{z}-heading" + heading_id = f"{prefix}-{y}-{z}-heading" self.assertContains( response, f'
' ) + self.assertContains( + response, + '

Some fields

', + ) + self.assertContains( + response, + '

' + "Some other fields

", + ) + self.assertContains( + response, + '

이름

', + ) post = self.client.post( reverse("admin:admin_views_article_add"), add_dict, follow=False ) diff --git a/tests/gis_tests/data/geoip2/GeoIP2-City-Test.mmdb b/tests/gis_tests/data/geoip2/GeoIP2-City-Test.mmdb index 3197ef122fa3..bf3cbe783593 100644 Binary files a/tests/gis_tests/data/geoip2/GeoIP2-City-Test.mmdb and b/tests/gis_tests/data/geoip2/GeoIP2-City-Test.mmdb differ diff --git a/tests/gis_tests/data/geoip2/GeoIP2-Country-Test.mmdb b/tests/gis_tests/data/geoip2/GeoIP2-Country-Test.mmdb index d79c9933bb98..a5989654ebd2 100644 Binary files a/tests/gis_tests/data/geoip2/GeoIP2-Country-Test.mmdb and b/tests/gis_tests/data/geoip2/GeoIP2-Country-Test.mmdb differ diff --git a/tests/gis_tests/data/geoip2/GeoLite2-ASN-Test.mmdb b/tests/gis_tests/data/geoip2/GeoLite2-ASN-Test.mmdb index afa7e956e4cd..fe2123f9e0fc 100644 Binary files a/tests/gis_tests/data/geoip2/GeoLite2-ASN-Test.mmdb and b/tests/gis_tests/data/geoip2/GeoLite2-ASN-Test.mmdb differ diff --git a/tests/gis_tests/data/geoip2/GeoLite2-City-Test.mmdb b/tests/gis_tests/data/geoip2/GeoLite2-City-Test.mmdb index 028a6984d93f..9eea131c76b7 100644 Binary files a/tests/gis_tests/data/geoip2/GeoLite2-City-Test.mmdb and b/tests/gis_tests/data/geoip2/GeoLite2-City-Test.mmdb differ diff --git a/tests/gis_tests/data/geoip2/GeoLite2-Country-Test.mmdb b/tests/gis_tests/data/geoip2/GeoLite2-Country-Test.mmdb index a2cbb0831697..0233bba39b82 100644 Binary files a/tests/gis_tests/data/geoip2/GeoLite2-Country-Test.mmdb and b/tests/gis_tests/data/geoip2/GeoLite2-Country-Test.mmdb differ diff --git a/tests/gis_tests/data/geoip2/LICENSE b/tests/gis_tests/data/geoip2/LICENSE deleted file mode 100644 index f86abbd73e1c..000000000000 --- a/tests/gis_tests/data/geoip2/LICENSE +++ /dev/null @@ -1,4 +0,0 @@ -This work is licensed under the Creative Commons Attribution-ShareAlike 3.0 -Unported License. To view a copy of this license, visit -http://creativecommons.org/licenses/by-sa/3.0/ or send a letter to Creative -Commons, 444 Castro Street, Suite 900, Mountain View, California, 94041, USA. diff --git a/tests/gis_tests/data/geoip2/README b/tests/gis_tests/data/geoip2/README deleted file mode 100644 index b6a21720a38d..000000000000 --- a/tests/gis_tests/data/geoip2/README +++ /dev/null @@ -1,3 +0,0 @@ -These test databases are taken from the following repository: - -https://github.com/maxmind/MaxMind-DB/ diff --git a/tests/gis_tests/data/geoip2/README.md b/tests/gis_tests/data/geoip2/README.md new file mode 100644 index 000000000000..f2a703b45775 --- /dev/null +++ b/tests/gis_tests/data/geoip2/README.md @@ -0,0 +1,28 @@ +# GeoIP2 and GeoLite2 Test Databases + +The following test databases are provided under [this license][0]: + +- `GeoIP2-City-Test.mmdb` +- `GeoIP2-Country-Test.mmdb` +- `GeoLite2-ASN-Test.mmdb` +- `GeoLite2-City-Test.mmdb` +- `GeoLite2-Country-Test.mmdb` + +Updates can be found in [this repository][1]. + +[0]: https://github.com/maxmind/MaxMind-DB/blob/main/LICENSE-MIT +[1]: https://github.com/maxmind/MaxMind-DB/tree/main/test-data + +# DB-IP Lite Test Databases + +The following test databases are provided under [this license][2]: + +- `dbip-city-lite-test.mmdb` +- `dbip-country-lite-test.mmdb` + +They have been modified to strip them down to a minimal dataset for testing. + +Updates can be found at [this download page][3] from DB-IP. + +[2]: https://creativecommons.org/licenses/by/4.0/ +[3]: https://db-ip.com/db/lite.php diff --git a/tests/gis_tests/data/geoip2/dbip-city-lite-test.mmdb b/tests/gis_tests/data/geoip2/dbip-city-lite-test.mmdb new file mode 100644 index 000000000000..5f0d657c841a Binary files /dev/null and b/tests/gis_tests/data/geoip2/dbip-city-lite-test.mmdb differ diff --git a/tests/gis_tests/data/geoip2/dbip-country-lite-test.mmdb b/tests/gis_tests/data/geoip2/dbip-country-lite-test.mmdb new file mode 100644 index 000000000000..8410d1893c5d Binary files /dev/null and b/tests/gis_tests/data/geoip2/dbip-country-lite-test.mmdb differ diff --git a/tests/gis_tests/test_geoip2.py b/tests/gis_tests/test_geoip2.py index 11c73bec0cd8..61b3565d1c72 100644 --- a/tests/gis_tests/test_geoip2.py +++ b/tests/gis_tests/test_geoip2.py @@ -32,6 +32,33 @@ class GeoLite2Test(SimpleTestCase): ipv6_addr = ipaddress.ip_address(ipv6_str) query_values = (fqdn, ipv4_str, ipv6_str, ipv4_addr, ipv6_addr) + expected_city = { + "accuracy_radius": 100, + "city": "Boxford", + "continent_code": "EU", + "continent_name": "Europe", + "country_code": "GB", + "country_name": "United Kingdom", + "is_in_european_union": False, + "latitude": 51.75, + "longitude": -1.25, + "metro_code": None, + "postal_code": "OX1", + "region_code": "ENG", + "region_name": "England", + "time_zone": "Europe/London", + # Kept for backward compatibility. + "dma_code": None, + "region": "ENG", + } + expected_country = { + "continent_code": "EU", + "continent_name": "Europe", + "country_code": "GB", + "country_name": "United Kingdom", + "is_in_european_union": False, + } + @classmethod def setUpClass(cls): # Avoid referencing __file__ at module level. @@ -97,88 +124,58 @@ def test_bad_query(self): def test_country(self): g = GeoIP2(city="") - self.assertIs(g._metadata.database_type.endswith("Country"), True) + self.assertIs(g.is_city, False) + self.assertIs(g.is_country, True) for query in self.query_values: with self.subTest(query=query): + self.assertEqual(g.country(query), self.expected_country) + self.assertEqual( + g.country_code(query), self.expected_country["country_code"] + ) self.assertEqual( - g.country(query), - { - "continent_code": "EU", - "continent_name": "Europe", - "country_code": "GB", - "country_name": "United Kingdom", - "is_in_european_union": False, - }, + g.country_name(query), self.expected_country["country_name"] ) - self.assertEqual(g.country_code(query), "GB") - self.assertEqual(g.country_name(query), "United Kingdom") def test_country_using_city_database(self): g = GeoIP2(country="") - self.assertIs(g._metadata.database_type.endswith("City"), True) + self.assertIs(g.is_city, True) + self.assertIs(g.is_country, False) for query in self.query_values: with self.subTest(query=query): + self.assertEqual(g.country(query), self.expected_country) + self.assertEqual( + g.country_code(query), self.expected_country["country_code"] + ) self.assertEqual( - g.country(query), - { - "continent_code": "EU", - "continent_name": "Europe", - "country_code": "GB", - "country_name": "United Kingdom", - "is_in_european_union": False, - }, + g.country_name(query), self.expected_country["country_name"] ) - self.assertEqual(g.country_code(query), "GB") - self.assertEqual(g.country_name(query), "United Kingdom") def test_city(self): g = GeoIP2(country="") - self.assertIs(g._metadata.database_type.endswith("City"), True) + self.assertIs(g.is_city, True) + self.assertIs(g.is_country, False) for query in self.query_values: with self.subTest(query=query): - self.assertEqual( - g.city(query), - { - "accuracy_radius": 100, - "city": "Boxford", - "continent_code": "EU", - "continent_name": "Europe", - "country_code": "GB", - "country_name": "United Kingdom", - "is_in_european_union": False, - "latitude": 51.75, - "longitude": -1.25, - "metro_code": None, - "postal_code": "OX1", - "region_code": "ENG", - "region_name": "England", - "time_zone": "Europe/London", - # Kept for backward compatibility. - "dma_code": None, - "region": "ENG", - }, - ) + self.assertEqual(g.city(query), self.expected_city) geom = g.geos(query) self.assertIsInstance(geom, GEOSGeometry) self.assertEqual(geom.srid, 4326) - self.assertEqual(geom.tuple, (-1.25, 51.75)) - self.assertEqual(g.lat_lon(query), (51.75, -1.25)) - self.assertEqual(g.lon_lat(query), (-1.25, 51.75)) + expected_lat = self.expected_city["latitude"] + expected_lon = self.expected_city["longitude"] + self.assertEqual(geom.tuple, (expected_lon, expected_lat)) + self.assertEqual(g.lat_lon(query), (expected_lat, expected_lon)) + self.assertEqual(g.lon_lat(query), (expected_lon, expected_lat)) + # Country queries should still work. + self.assertEqual(g.country(query), self.expected_country) self.assertEqual( - g.country(query), - { - "continent_code": "EU", - "continent_name": "Europe", - "country_code": "GB", - "country_name": "United Kingdom", - "is_in_european_union": False, - }, + g.country_code(query), self.expected_country["country_code"] + ) + self.assertEqual( + g.country_name(query), self.expected_country["country_name"] ) - self.assertEqual(g.country_code(query), "GB") - self.assertEqual(g.country_name(query), "United Kingdom") def test_not_found(self): g1 = GeoIP2(city="") @@ -230,6 +227,27 @@ class GeoIP2Test(GeoLite2Test): """Non-free GeoIP2 databases are supported.""" +@skipUnless(HAS_GEOIP2, "GeoIP2 is required.") +@override_settings( + GEOIP_CITY="dbip-city-lite-test.mmdb", + GEOIP_COUNTRY="dbip-country-lite-test.mmdb", +) +class DBIPLiteTest(GeoLite2Test): + """DB-IP Lite databases are supported.""" + + expected_city = GeoLite2Test.expected_city | { + "accuracy_radius": None, + "city": "London (Shadwell)", + "latitude": 51.5181, + "longitude": -0.0714189, + "postal_code": None, + "region_code": None, + "time_zone": None, + # Kept for backward compatibility. + "region": None, + } + + @skipUnless(HAS_GEOIP2, "GeoIP2 is required.") class ErrorTest(SimpleTestCase): def test_missing_path(self): diff --git a/tests/user_commands/tests.py b/tests/user_commands/tests.py index 65e176620db1..2a1e904f3bda 100644 --- a/tests/user_commands/tests.py +++ b/tests/user_commands/tests.py @@ -400,8 +400,8 @@ def test_subparser_dest_required_args(self): self.assertIn("bar", out.getvalue()) def test_subparser_invalid_option(self): - msg = "invalid choice: 'test' (choose from 'foo')" - with self.assertRaisesMessage(CommandError, msg): + msg = r"invalid choice: 'test' \(choose from '?foo'?\)" + with self.assertRaisesRegex(CommandError, msg): management.call_command("subparser", "test", 12) msg = "Error: the following arguments are required: subcommand" with self.assertRaisesMessage(CommandError, msg): diff --git a/tests/validators/tests.py b/tests/validators/tests.py index ba1db5ea46d3..4ae0f6413e5e 100644 --- a/tests/validators/tests.py +++ b/tests/validators/tests.py @@ -635,8 +635,8 @@ (validate_domain_name, "python-python.com", None), (validate_domain_name, "python.name.uk", None), (validate_domain_name, "python.tips", None), - (validate_domain_name, "http://例子.测试", None), - (validate_domain_name, "http://dashinpunytld.xn---c", None), + (validate_domain_name, "例子.测试", None), + (validate_domain_name, "dashinpunytld.xn---c", None), (validate_domain_name, "python..org", ValidationError), (validate_domain_name, "python-.org", ValidationError), (validate_domain_name, "too-long-name." * 20 + "com", ValidationError), @@ -652,6 +652,16 @@ ), (DomainNameValidator(accept_idna=False), "ıçğü.com", ValidationError), (DomainNameValidator(accept_idna=False), "not-domain-name", ValidationError), + ( + DomainNameValidator(accept_idna=False), + "not-domain-name, but-has-domain-name-suffix.com", + ValidationError, + ), + ( + DomainNameValidator(accept_idna=False), + "not-domain-name.com, but has domain prefix", + ValidationError, + ), ] # Add valid and invalid URL tests. diff --git a/tox.ini b/tox.ini index c635a129b2d1..7a76693f2116 100644 --- a/tox.ini +++ b/tox.ini @@ -26,7 +26,7 @@ setenv = PYTHONDONTWRITEBYTECODE=1 deps = -e . - py{3,310,311,312,py3}: -rtests/requirements/py3.txt + py{3,310,311,312,313,py3}: -rtests/requirements/py3.txt postgres: -rtests/requirements/postgres.txt mysql: -rtests/requirements/mysql.txt oracle: -rtests/requirements/oracle.txt