From 7f5be5009c447b0c907e6f3eec9042cbb094a92e Mon Sep 17 00:00:00 2001 From: Bernardo Fontes Date: Mon, 27 Oct 2025 11:27:25 +0100 Subject: [PATCH 1/9] Ignore .envrc file used to configure direnv --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 17607d43..12800b06 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ pyvenv.cfg dist/ /.coverage /.coverage.* +.envrc From ad10b30ab9a1264afc9eff3890f8f2a2fb88c510 Mon Sep 17 00:00:00 2001 From: Bernardo Fontes Date: Mon, 27 Oct 2025 11:27:43 +0100 Subject: [PATCH 2/9] Refactor test to avoid model definition hardcoded in test --- tests/generic/models.py | 5 +++++ tests/test_baker.py | 9 ++------- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/generic/models.py b/tests/generic/models.py index 4c14c373..78f3eb7f 100755 --- a/tests/generic/models.py +++ b/tests/generic/models.py @@ -154,6 +154,11 @@ class Person(models.Model): geom_collection = models.GeometryCollectionField() +class ProxyToPersonModel(Person): + class Meta: + proxy = True + + class Dog(models.Model): class Meta: order_with_respect_to = "owner" diff --git a/tests/test_baker.py b/tests/test_baker.py index d9c52092..d40bd8e9 100644 --- a/tests/test_baker.py +++ b/tests/test_baker.py @@ -652,19 +652,14 @@ def get_dummy_key(): def test_create_model_with_contenttype_field_and_proxy_model(self): from django.contrib.contenttypes.models import ContentType - class ProxyPerson(models.Person): - class Meta: - proxy = True - app_label = "generic" - dummy = baker.make( models.DummyGenericForeignKeyModel, - content_object=baker.make(ProxyPerson, name="John Doe"), + content_object=baker.make(models.ProxyToPersonModel, name="John Doe"), ) dummy.refresh_from_db() assert isinstance(dummy, models.DummyGenericForeignKeyModel) assert isinstance(dummy.content_type, ContentType) - assert isinstance(dummy.content_object, ProxyPerson) + assert isinstance(dummy.content_object, models.ProxyToPersonModel) assert dummy.content_object.name == "John Doe" From 95b5a52ce621824e9eb3982c9ac05179f870faa6 Mon Sep 17 00:00:00 2001 From: Bernardo Fontes Date: Mon, 27 Oct 2025 11:57:59 +0100 Subject: [PATCH 3/9] Respects Django's default behavior of relying on concrete models by default --- model_bakery/baker.py | 2 +- tests/test_baker.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/model_bakery/baker.py b/model_bakery/baker.py index 8b963ef8..05757293 100644 --- a/model_bakery/baker.py +++ b/model_bakery/baker.py @@ -719,7 +719,7 @@ def _handle_generic_foreign_keys(self, instance: Model, attrs: dict[str, Any]): instance, ct_field_name, contenttypes_models.ContentType.objects.get_for_model( - value, for_concrete_model=False + value, for_concrete_model=True ), ) setattr(instance, oid_field_name, value.pk) diff --git a/tests/test_baker.py b/tests/test_baker.py index d40bd8e9..49e064a1 100644 --- a/tests/test_baker.py +++ b/tests/test_baker.py @@ -649,7 +649,9 @@ def get_dummy_key(): assert isinstance(dummy.content_type, ContentType) assert isinstance(dummy.content_object, models.Person) - def test_create_model_with_contenttype_field_and_proxy_model(self): + def test_create_model_with_contenttype_field_and_proxy_model_for_concret_model( + self, + ): from django.contrib.contenttypes.models import ContentType dummy = baker.make( @@ -659,7 +661,7 @@ def test_create_model_with_contenttype_field_and_proxy_model(self): dummy.refresh_from_db() assert isinstance(dummy, models.DummyGenericForeignKeyModel) assert isinstance(dummy.content_type, ContentType) - assert isinstance(dummy.content_object, models.ProxyToPersonModel) + assert isinstance(dummy.content_object, models.Person) assert dummy.content_object.name == "John Doe" From 24f7d39258118b7464f2c95585bbc7edd738e221 Mon Sep 17 00:00:00 2001 From: Bernardo Fontes Date: Mon, 27 Oct 2025 12:04:34 +0100 Subject: [PATCH 4/9] Generic foreign key creation respects their for_concrete_model flag --- model_bakery/baker.py | 3 ++- tests/generic/models.py | 12 ++++++++++++ tests/test_baker.py | 9 +++++++-- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/model_bakery/baker.py b/model_bakery/baker.py index 05757293..0a76ed53 100644 --- a/model_bakery/baker.py +++ b/model_bakery/baker.py @@ -521,6 +521,7 @@ def instance( "value": attrs.pop(k), "content_type_field": field.ct_field, "object_id_field": field.fk_field, + "for_concrete_model": field.for_concrete_model, } instance = self.model(**attrs) @@ -719,7 +720,7 @@ def _handle_generic_foreign_keys(self, instance: Model, attrs: dict[str, Any]): instance, ct_field_name, contenttypes_models.ContentType.objects.get_for_model( - value, for_concrete_model=True + value, for_concrete_model=data["for_concrete_model"] ), ) setattr(instance, oid_field_name, value.pk) diff --git a/tests/generic/models.py b/tests/generic/models.py index 78f3eb7f..bcba9326 100755 --- a/tests/generic/models.py +++ b/tests/generic/models.py @@ -307,6 +307,18 @@ class DummyGenericForeignKeyModel(models.Model): object_id = models.PositiveIntegerField() content_object = GenericForeignKey("content_type", "object_id") + proxy_content_type = models.ForeignKey( + contenttypes.ContentType, + on_delete=models.CASCADE, + blank=True, + null=True, + limit_choices_to={"model__in": ["person", "dog"]}, + ) + proxy_object_id = models.PositiveIntegerField() + proxy_content_object = GenericForeignKey( + "proxy_content_type", "proxy_object_id", for_concrete_model=False + ) + class DummyGenericRelationModel(models.Model): relation = GenericRelation(DummyGenericForeignKeyModel) diff --git a/tests/test_baker.py b/tests/test_baker.py index 49e064a1..71786bf3 100644 --- a/tests/test_baker.py +++ b/tests/test_baker.py @@ -649,20 +649,25 @@ def get_dummy_key(): assert isinstance(dummy.content_type, ContentType) assert isinstance(dummy.content_object, models.Person) - def test_create_model_with_contenttype_field_and_proxy_model_for_concret_model( + def test_create_model_with_contenttype_field_and_proxy_model_respecting_generic_fK_config( self, ): from django.contrib.contenttypes.models import ContentType + proxy_person = baker.make(models.ProxyToPersonModel, name="John Doe") dummy = baker.make( models.DummyGenericForeignKeyModel, - content_object=baker.make(models.ProxyToPersonModel, name="John Doe"), + content_object=proxy_person, + proxy_content_object=proxy_person, ) dummy.refresh_from_db() assert isinstance(dummy, models.DummyGenericForeignKeyModel) assert isinstance(dummy.content_type, ContentType) assert isinstance(dummy.content_object, models.Person) assert dummy.content_object.name == "John Doe" + assert isinstance(dummy.proxy_content_type, ContentType) + assert isinstance(dummy.proxy_content_object, models.ProxyToPersonModel) + assert dummy.proxy_content_object.name == "John Doe" @pytest.mark.skipif( From e302ce6d7d6708d0659066e0f6b0f4712f9f0e6d Mon Sep 17 00:00:00 2001 From: Bernardo Fontes Date: Mon, 27 Oct 2025 12:09:32 +0100 Subject: [PATCH 5/9] Fix typo --- docs/basic_usage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/basic_usage.md b/docs/basic_usage.md index e15170c3..18b8ef15 100644 --- a/docs/basic_usage.md +++ b/docs/basic_usage.md @@ -106,7 +106,7 @@ class PurchaseHistoryTestModel(TestCase): It will also create the Customer, automagically. -**NOTE: ForeignKeys and OneToOneFields** - Since Django 1.8, ForeignKey and OneToOne fields don't accept unpersisted model instances anymore. This means that if you run: +**NOTE: ForeignKeys and OneToOneFields** - Since Django 1.8, ForeignKey and OneToOne fields don't accept non persisted model instances anymore. This means that if you run: ```python baker.prepare('shop.PurchaseHistory') From 029f9817d8691d5d72c9592fe17300bca858c695 Mon Sep 17 00:00:00 2001 From: Bernardo Fontes Date: Mon, 27 Oct 2025 12:09:50 +0100 Subject: [PATCH 6/9] Add docs on the lib's behavior with Generic Foreign Keys relations --- docs/basic_usage.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/basic_usage.md b/docs/basic_usage.md index 18b8ef15..6dbf4055 100644 --- a/docs/basic_usage.md +++ b/docs/basic_usage.md @@ -108,6 +108,9 @@ It will also create the Customer, automagically. **NOTE: ForeignKeys and OneToOneFields** - Since Django 1.8, ForeignKey and OneToOne fields don't accept non persisted model instances anymore. This means that if you run: +**NOTE: GenericForeignKey** - `model-bakery` defines the content type for this relation based in how +the relation configures their [`for_concrete_model` flag](https://docs.djangoproject.com/en/5.2/ref/contrib/contenttypes/#django.contrib.contenttypes.fields.GenericForeignKey.for_concrete_model). + ```python baker.prepare('shop.PurchaseHistory') ``` From 5098f358ebdbfbd1890b8b4874217bf2570d578a Mon Sep 17 00:00:00 2001 From: Bernardo Fontes Date: Mon, 27 Oct 2025 12:10:02 +0100 Subject: [PATCH 7/9] Update the README --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ac3734c..2a769dfb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,9 +9,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Added - Add Python 3.14 support - Add Django 6.0 support +### Changed ### Changed - [dev] Various improvements to CI Python/Django matrices +- The creation of generic foreign key fields now respects their `for_concrete_model` configuration ### Removed - Drop fallbacks made for Django < 4.2 From 2e4f5e1eab0b357b719cbd762acbd639a2e0c1d1 Mon Sep 17 00:00:00 2001 From: Bernardo Fontes Date: Mon, 27 Oct 2025 12:19:11 +0100 Subject: [PATCH 8/9] Wooops. I duplicated this section in the changelog by mistake --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a769dfb..2ee179ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,6 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Added - Add Python 3.14 support - Add Django 6.0 support -### Changed ### Changed - [dev] Various improvements to CI Python/Django matrices From 6c1ab27f5cdfb6cc8ee5267421f95d6a4e1a1945 Mon Sep 17 00:00:00 2001 From: Bernardo Fontes Date: Tue, 28 Oct 2025 10:28:05 +0100 Subject: [PATCH 9/9] Properly place new note in the docs --- docs/basic_usage.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/basic_usage.md b/docs/basic_usage.md index 6dbf4055..978461c9 100644 --- a/docs/basic_usage.md +++ b/docs/basic_usage.md @@ -108,15 +108,15 @@ It will also create the Customer, automagically. **NOTE: ForeignKeys and OneToOneFields** - Since Django 1.8, ForeignKey and OneToOne fields don't accept non persisted model instances anymore. This means that if you run: -**NOTE: GenericForeignKey** - `model-bakery` defines the content type for this relation based in how -the relation configures their [`for_concrete_model` flag](https://docs.djangoproject.com/en/5.2/ref/contrib/contenttypes/#django.contrib.contenttypes.fields.GenericForeignKey.for_concrete_model). - ```python baker.prepare('shop.PurchaseHistory') ``` You'll end up with a persisted "Customer" instance. +**NOTE: GenericForeignKey** - `model-bakery` defines the content type for this relation based in how +the relation configures their [`for_concrete_model` flag](https://docs.djangoproject.com/en/5.2/ref/contrib/contenttypes/#django.contrib.contenttypes.fields.GenericForeignKey.for_concrete_model). + ## M2M Relationships By default, Model Bakery doesn't create related instances for many-to-many relationships.