Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion django/forms/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
75 changes: 4 additions & 71 deletions docs/intro/tutorial02.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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 <check>`; this checks for any problems in
your project without making migrations or touching the database.
Expand Down Expand Up @@ -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 </ref/django-admin>` for full
information on what the ``manage.py`` utility can do.

Expand Down
15 changes: 10 additions & 5 deletions docs/topics/db/models.txt
Original file line number Diff line number Diff line change
Expand Up @@ -39,24 +39,29 @@ 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
);

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 </topics/settings>`.
syntax, but Django uses SQL tailored to the database backend specified in
your :doc:`settings file </topics/settings>`. 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
============
Expand Down
81 changes: 81 additions & 0 deletions docs/topics/migrations.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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`:

Expand Down Expand Up @@ -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
---------------

Expand Down
9 changes: 9 additions & 0 deletions tests/backends/oracle/test_creation.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os
import unittest
from io import StringIO
from unittest import mock
Expand Down Expand Up @@ -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"
Expand All @@ -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(
Expand Down
53 changes: 53 additions & 0 deletions tests/model_formsets/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 = {
Expand Down
Loading