diff --git a/django/forms/models.py b/django/forms/models.py index e16abc90fa84..3a7b7973e63c 100644 --- a/django/forms/models.py +++ b/django/forms/models.py @@ -736,7 +736,14 @@ def _construct_form(self, i, **kwargs): pk_required = i < self.initial_form_count() if pk_required: if self.is_bound: - pk_key = "%s-%s" % (self.add_prefix(i), self.model._meta.pk.name) + # Avoid initializing the form only to compute its prefix when + # it uses BaseForm.add_prefix(). + if self.form.add_prefix is BaseForm.add_prefix: + pk_key = "%s-%s" % (self.add_prefix(i), self.model._meta.pk.name) + else: + form_kwargs = {"prefix": self.add_prefix(i), **kwargs} + pk_form = self.form(**form_kwargs) + pk_key = pk_form.add_prefix(self.model._meta.pk.name) try: pk = self.data[pk_key] except KeyError: diff --git a/docs/intro/tutorial02.txt b/docs/intro/tutorial02.txt index f6929266b7bc..084017aea465 100644 --- a/docs/intro/tutorial02.txt +++ b/docs/intro/tutorial02.txt @@ -223,77 +223,6 @@ Don't worry, you're not expected to read them every time Django makes one, but they're designed to be human-editable in case you want to manually tweak how Django changes things. -There's a command that will run the migrations for you and manage your database -schema automatically - that's called :djadmin:`migrate`, and we'll come to it -in a moment - but first, let's see what SQL that migration would run. The -:djadmin:`sqlmigrate` command takes migration names and returns their SQL: - -.. console:: - - $ python manage.py sqlmigrate polls 0001 - -You should see something similar to the following (we've reformatted it for -readability): - -.. code-block:: sql - - BEGIN; - -- - -- Create model Question - -- - CREATE TABLE "polls_question" ( - "id" bigint NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, - "question_text" varchar(200) NOT NULL, - "pub_date" timestamp with time zone NOT NULL - ); - -- - -- Create model Choice - -- - CREATE TABLE "polls_choice" ( - "id" bigint NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, - "choice_text" varchar(200) NOT NULL, - "votes" integer NOT NULL, - "question_id" bigint NOT NULL - ); - ALTER TABLE "polls_choice" - ADD CONSTRAINT "polls_choice_question_id_c5b4b260_fk_polls_question_id" - FOREIGN KEY ("question_id") - REFERENCES "polls_question" ("id") - DEFERRABLE INITIALLY DEFERRED; - CREATE INDEX "polls_choice_question_id_c5b4b260" ON "polls_choice" ("question_id"); - - COMMIT; - -Note the following: - -* The exact output will vary depending on the database you are using. The - example above is generated for PostgreSQL. - -* Table names are automatically generated by combining the name of the app - (``polls``) and the lowercase name of the model -- ``question`` and - ``choice``. (You can override this behavior.) - -* Primary keys (IDs) are added automatically. (You can override this, too.) - -* By convention, Django appends ``"_id"`` to the foreign key field name. - (Yes, you can override this, as well.) - -* The foreign key relationship is made explicit by a ``FOREIGN KEY`` - constraint. Don't worry about the ``DEFERRABLE`` parts; it's telling - PostgreSQL to not enforce the foreign key until the end of the transaction. - -* It's tailored to the database you're using, so database-specific field types - such as ``auto_increment`` (MySQL), ``bigint PRIMARY KEY GENERATED BY DEFAULT - AS IDENTITY`` (PostgreSQL), or ``integer primary key autoincrement`` (SQLite) - are handled for you automatically. Same goes for the quoting of field names - -- e.g., using double quotes or single quotes. - -* The :djadmin:`sqlmigrate` command doesn't actually run the migration on your - database - instead, it prints it to the screen so that you can see what SQL - Django thinks is required. It's useful for checking what Django is going to - do or if you have database administrators who require SQL scripts for - changes. - If you're interested, you can also run :djadmin:`python manage.py check `; this checks for any problems in your project without making migrations or touching the database. @@ -332,6 +261,10 @@ because you'll commit migrations to your version control system and ship them with your app; they not only make your development easier, they're also usable by other developers and in production. +See :doc:`/topics/migrations` for the full details, including how to inspect +the SQL a migration will run with :djadmin:`sqlmigrate` (see +:ref:`executing-sqlmigrate`). + Read the :doc:`django-admin documentation ` for full information on what the ``manage.py`` utility can do. diff --git a/docs/topics/db/models.txt b/docs/topics/db/models.txt index 45156d8a16ef..70f9281adeaf 100644 --- a/docs/topics/db/models.txt +++ b/docs/topics/db/models.txt @@ -39,7 +39,7 @@ The above ``Person`` model would create a database table like this: .. code-block:: sql - CREATE TABLE myapp_person ( + CREATE TABLE "myapp_person" ( "id" bigint NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, "first_name" varchar(30) NOT NULL, "last_name" varchar(30) NOT NULL @@ -47,16 +47,21 @@ The above ``Person`` model would create a database table like this: Some technical notes: -* The name of the table, ``myapp_person``, is automatically derived from - some model metadata but can be overridden. See :ref:`table-names` for more +* By default, the name of the table (``myapp_person``) is automatically derived + from model metadata, combining the name of the app (``myapp``) and the + lowercase name of the model (``person``). See :ref:`table-names` for more details. * An ``id`` field is added automatically, but this behavior can be overridden. See :ref:`automatic-primary-key-fields`. * The ``CREATE TABLE`` SQL in this example is formatted using PostgreSQL - syntax, but it's worth noting Django uses SQL tailored to the database - backend specified in your :doc:`settings file `. + syntax, but Django uses SQL tailored to the database backend specified in + your :doc:`settings file `. Database-specific field types + such as ``auto_increment`` (MySQL), ``bigint PRIMARY KEY GENERATED BY + DEFAULT AS IDENTITY`` (PostgreSQL), or ``integer primary key autoincrement`` + (SQLite) are handled for you automatically, as is the quoting of field + names. Using models ============ diff --git a/docs/topics/migrations.txt b/docs/topics/migrations.txt index 285fb192cd52..0209c70ed5dd 100644 --- a/docs/topics/migrations.txt +++ b/docs/topics/migrations.txt @@ -110,6 +110,19 @@ Django projects without the need for a full database. Workflow ======== +The examples in this section use a ``books`` app with the following models:: + + from django.db import models + + + class Author(models.Model): + name = models.CharField(max_length=100) + + + class Book(models.Model): + title = models.CharField(max_length=100) + author = models.ForeignKey(Author, on_delete=models.CASCADE) + Django can create migrations for you. Make changes to your models - say, add a field and remove a model - and then run :djadmin:`makemigrations`: @@ -150,6 +163,74 @@ one, you can use the :option:`makemigrations --name` option: $ python manage.py makemigrations --name changed_my_model your_app_label +.. _executing-sqlmigrate: + +Executing ``sqlmigrate`` +------------------------ + +The :djadmin:`sqlmigrate` command takes migration names and returns the SQL +they would run. It doesn't actually run the migration on your database - +instead, it prints it to the screen so that you can see what SQL Django thinks +is required. It's useful for checking what Django is going to do or if you have +database administrators who require SQL scripts for changes. + +To inspect the SQL of the initial migration that created the tables for the +``books`` models above, run: + +.. code-block:: shell + + $ python manage.py sqlmigrate books 0001 + +You should see something similar to the following (we've reformatted it for +readability): + +.. code-block:: sql + + BEGIN; + -- + -- Create model Author + -- + CREATE TABLE "books_author" ( + "id" bigint NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, + "name" varchar(100) NOT NULL + ); + -- + -- Create model Book + -- + CREATE TABLE "books_book" ( + "id" bigint NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, + "title" varchar(100) NOT NULL, + "author_id" bigint NOT NULL + ); + ALTER TABLE "books_book" + ADD CONSTRAINT "books_book_author_id_8b91747b_fk_books_author_id" + FOREIGN KEY ("author_id") + REFERENCES "books_author" ("id") + DEFERRABLE INITIALLY DEFERRED; + CREATE INDEX "books_book_author_id_8b91747b" ON "books_book" ("author_id"); + + COMMIT; + +Note the following: + +* The exact output will vary depending on the database you are using, since + Django tailors the SQL to it. Database-specific field types and the quoting + of names are handled for you automatically. The example above is generated + for PostgreSQL. + +* Table names are automatically generated by combining the name of the app + (``books``) with the lowercase name of each model, giving ``books_author`` + and ``books_book``. (You can override this behavior.) + +* Primary keys (IDs) are added automatically. (You can override this, too.) + +* By convention, Django appends ``"_id"`` to the foreign key field name. + (Yes, you can override this, as well.) + +* The foreign key relationship is made explicit by a ``FOREIGN KEY`` + constraint. Don't worry about the ``DEFERRABLE`` parts; it's telling + PostgreSQL to not enforce the foreign key until the end of the transaction. + Version control --------------- diff --git a/tests/backends/oracle/test_creation.py b/tests/backends/oracle/test_creation.py index 6f6b9fd23325..87866e50ce80 100644 --- a/tests/backends/oracle/test_creation.py +++ b/tests/backends/oracle/test_creation.py @@ -1,3 +1,4 @@ +import os import unittest from io import StringIO from unittest import mock @@ -41,6 +42,10 @@ def patch_execute_statements(self, execute_statements): ) @mock.patch.object(DatabaseCreation, "_test_user_create", return_value=False) + @unittest.skipUnless( + os.environ.get("TESTPILOT_USER") is not None, + "Not possible with Oracle Test Pilot", + ) def test_create_test_db(self, *mocked_objects): creation = DatabaseCreation(connection) # Simulate test database creation raising "tablespace already exists" @@ -62,6 +67,10 @@ def test_create_test_db(self, *mocked_objects): creation._create_test_db(verbosity=0, keepdb=True) @mock.patch.object(DatabaseCreation, "_test_database_create", return_value=False) + @unittest.skipUnless( + os.environ.get("TESTPILOT_USER") is not None, + "Not possible with Oracle Test Pilot", + ) def test_create_test_user(self, *mocked_objects): creation = DatabaseCreation(connection) with mock.patch.object( diff --git a/tests/model_formsets/tests.py b/tests/model_formsets/tests.py index 590ebd530a44..edddace4ae18 100644 --- a/tests/model_formsets/tests.py +++ b/tests/model_formsets/tests.py @@ -2,6 +2,7 @@ import re from datetime import date from decimal import Decimal +from unittest import mock from django import forms from django.core.exceptions import ImproperlyConfigured @@ -1988,6 +1989,58 @@ def test_prevent_change_outer_model_and_create_invalid_data(self): # created. self.assertSequenceEqual(Author.objects.all(), [author, other_author]) + def test_overridden_add_prefix(self): + class AuthorForm(forms.ModelForm): + class Meta: + model = Author + fields = "__all__" + + def add_prefix(self, field_name): + return f"{self.prefix}.{field_name}" if self.prefix else field_name + + author = Author.objects.create(name="Charles Baudelaire") + AuthorFormSet = modelformset_factory(Author, form=AuthorForm) + data = { + "form-TOTAL_FORMS": "1", + "form-INITIAL_FORMS": "1", + "form-MAX_NUM_FORMS": "0", + "form-0.id": str(author.pk), + "form-0.name": "Charles P. Baudelaire", + } + formset = AuthorFormSet(data, queryset=Author.objects.all()) + + self.assertIs(formset.is_valid(), True) + self.assertEqual(formset.forms[0].instance, author) + formset.save() + author.refresh_from_db() + self.assertEqual(author.name, "Charles P. Baudelaire") + + def test_form_initialized_once_with_default_add_prefix(self): + initialization = mock.Mock() + + class AuthorForm(forms.ModelForm): + class Meta: + model = Author + fields = "__all__" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + initialization() + + author = Author.objects.create(name="Charles Baudelaire") + AuthorFormSet = modelformset_factory(Author, form=AuthorForm) + formset = AuthorFormSet( + { + "form-TOTAL_FORMS": "1", + "form-INITIAL_FORMS": "1", + "form-0-id": str(author.pk), + }, + queryset=Author.objects.all(), + ) + + self.assertEqual(formset.forms[0].instance, author) + initialization.assert_called_once_with() + def test_validation_without_id(self): AuthorFormSet = modelformset_factory(Author, fields="__all__") data = {