diff --git a/django/contrib/contenttypes/fields.py b/django/contrib/contenttypes/fields.py index b70a947ddf..a25aa64cb9 100644 --- a/django/contrib/contenttypes/fields.py +++ b/django/contrib/contenttypes/fields.py @@ -523,6 +523,20 @@ def create_generic_related_manager(superclass): return super(GenericRelatedObjectManager, self).using(db).create(**kwargs) create.alters_data = True + def get_or_create(self, **kwargs): + kwargs[self.content_type_field_name] = self.content_type + kwargs[self.object_id_field_name] = self.pk_val + db = router.db_for_write(self.model, instance=self.instance) + return super(GenericRelatedObjectManager, self).using(db).get_or_create(**kwargs) + get_or_create.alters_data = True + + def update_or_create(self, **kwargs): + kwargs[self.content_type_field_name] = self.content_type + kwargs[self.object_id_field_name] = self.pk_val + db = router.db_for_write(self.model, instance=self.instance) + return super(GenericRelatedObjectManager, self).using(db).update_or_create(**kwargs) + update_or_create.alters_data = True + return GenericRelatedObjectManager diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index 868f43090d..d87a9229ef 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -715,13 +715,17 @@ def create_foreign_related_manager(superclass, rel_field, rel_model): create.alters_data = True def get_or_create(self, **kwargs): - # Update kwargs with the related object that this - # ForeignRelatedObjectsDescriptor knows about. kwargs[rel_field.name] = self.instance db = router.db_for_write(self.model, instance=self.instance) return super(RelatedManager, self.db_manager(db)).get_or_create(**kwargs) get_or_create.alters_data = True + def update_or_create(self, **kwargs): + kwargs[rel_field.name] = self.instance + db = router.db_for_write(self.model, instance=self.instance) + return super(RelatedManager, self.db_manager(db)).update_or_create(**kwargs) + update_or_create.alters_data = True + # remove() and clear() are only provided if the ForeignKey can have a value of null. if rel_field.null: def remove(self, *objs, **kwargs): @@ -963,8 +967,7 @@ def create_many_related_manager(superclass, rel): def get_or_create(self, **kwargs): db = router.db_for_write(self.instance.__class__, instance=self.instance) - obj, created = \ - super(ManyRelatedManager, self.db_manager(db)).get_or_create(**kwargs) + obj, created = super(ManyRelatedManager, self.db_manager(db)).get_or_create(**kwargs) # We only need to add() if created because if we got an object back # from get() then the relationship already exists. if created: @@ -972,6 +975,16 @@ def create_many_related_manager(superclass, rel): return obj, created get_or_create.alters_data = True + def update_or_create(self, **kwargs): + db = router.db_for_write(self.instance.__class__, instance=self.instance) + obj, created = super(ManyRelatedManager, self.db_manager(db)).update_or_create(**kwargs) + # We only need to add() if created because if we got an object back + # from get() then the relationship already exists. + if created: + self.add(obj) + return obj, created + update_or_create.alters_data = True + def _add_items(self, source_field_name, target_field_name, *objs): # source_field_name: the PK fieldname in join table for the source object # target_field_name: the PK fieldname in join table for the target object diff --git a/docs/releases/1.7.1.txt b/docs/releases/1.7.1.txt index 1604e75da8..6511074542 100644 --- a/docs/releases/1.7.1.txt +++ b/docs/releases/1.7.1.txt @@ -100,3 +100,6 @@ Bugfixes * Fixed ``UnicodeDecodeError`` crash in ``AdminEmailHandler`` with non-ASCII characters in the request (:ticket:`23593`). + +* Fixed missing ``get_or_create`` and ``update_or_create`` on related managers + causing ``IntegrityError`` (:ticket:`23611`). diff --git a/tests/generic_relations/tests.py b/tests/generic_relations/tests.py index 3e05bb7225..3508a1096b 100644 --- a/tests/generic_relations/tests.py +++ b/tests/generic_relations/tests.py @@ -309,6 +309,64 @@ class GenericRelationsTests(TestCase): TaggedItem.objects.get(content_object='') +class GetOrCreateAndUpdateOrCreateTests(TestCase): + """ + GenericRelationsTests has changed significantly on master, this + standalone TestCase is part of the backport for #23611. + """ + def setUp(self): + self.bacon = Vegetable.objects.create(name="Bacon", is_yucky=False) + self.bacon.tags.create(tag="fatty") + self.bacon.tags.create(tag="salty") + + def test_generic_update_or_create_when_created(self): + """ + Should be able to use update_or_create from the generic related manager + to create a tag. Refs #23611. + """ + count = self.bacon.tags.count() + tag, created = self.bacon.tags.update_or_create(tag='stinky') + self.assertTrue(created) + self.assertEqual(count + 1, self.bacon.tags.count()) + + def test_generic_update_or_create_when_updated(self): + """ + Should be able to use update_or_create from the generic related manager + to update a tag. Refs #23611. + """ + count = self.bacon.tags.count() + tag = self.bacon.tags.create(tag='stinky') + self.assertEqual(count + 1, self.bacon.tags.count()) + tag, created = self.bacon.tags.update_or_create(defaults={'tag': 'juicy'}, id=tag.id) + self.assertFalse(created) + self.assertEqual(count + 1, self.bacon.tags.count()) + self.assertEqual(tag.tag, 'juicy') + + def test_generic_get_or_create_when_created(self): + """ + Should be able to use get_or_create from the generic related manager + to create a tag. Refs #23611. + """ + count = self.bacon.tags.count() + tag, created = self.bacon.tags.get_or_create(tag='stinky') + self.assertTrue(created) + self.assertEqual(count + 1, self.bacon.tags.count()) + + def test_generic_get_or_create_when_exists(self): + """ + Should be able to use get_or_create from the generic related manager + to get a tag. Refs #23611. + """ + count = self.bacon.tags.count() + tag = self.bacon.tags.create(tag="stinky") + self.assertEqual(count + 1, self.bacon.tags.count()) + tag, created = self.bacon.tags.get_or_create(id=tag.id, defaults={'tag': 'juicy'}) + self.assertFalse(created) + self.assertEqual(count + 1, self.bacon.tags.count()) + # shouldn't had changed the tag + self.assertEqual(tag.tag, 'stinky') + + class CustomWidget(forms.TextInput): pass diff --git a/tests/get_or_create/models.py b/tests/get_or_create/models.py index 1a85de2e74..915243a9c6 100644 --- a/tests/get_or_create/models.py +++ b/tests/get_or_create/models.py @@ -42,3 +42,17 @@ class Tag(models.Model): class Thing(models.Model): name = models.CharField(max_length=256) tags = models.ManyToManyField(Tag) + + +class Publisher(models.Model): + name = models.CharField(max_length=100) + + +class Author(models.Model): + name = models.CharField(max_length=100) + + +class Book(models.Model): + name = models.CharField(max_length=100) + authors = models.ManyToManyField(Author, related_name='books') + publisher = models.ForeignKey(Publisher, related_name='books', db_column="publisher_id_column") diff --git a/tests/get_or_create/tests.py b/tests/get_or_create/tests.py index 1ddb2447fe..3a9a5a5a42 100644 --- a/tests/get_or_create/tests.py +++ b/tests/get_or_create/tests.py @@ -8,7 +8,8 @@ from django.db import IntegrityError, DatabaseError from django.utils.encoding import DjangoUnicodeDecodeError from django.test import TestCase, TransactionTestCase -from .models import DefaultPerson, Person, ManualPrimaryKeyTest, Profile, Tag, Thing +from .models import (DefaultPerson, Person, ManualPrimaryKeyTest, Profile, + Tag, Thing, Publisher, Author, Book) class GetOrCreateTests(TestCase): @@ -199,3 +200,54 @@ class UpdateOrCreateTests(TestCase): except IntegrityError: formatted_traceback = traceback.format_exc() self.assertIn('obj.save', formatted_traceback) + + def test_create_with_related_manager(self): + """ + Should be able to use update_or_create from the related manager to + create a book. Refs #23611. + """ + p = Publisher.objects.create(name="Acme Publishing") + book, created = p.books.update_or_create(name="The Book of Ed & Fred") + self.assertTrue(created) + self.assertEqual(p.books.count(), 1) + + def test_update_with_related_manager(self): + """ + Should be able to use update_or_create from the related manager to + update a book. Refs #23611. + """ + p = Publisher.objects.create(name="Acme Publishing") + book = Book.objects.create(name="The Book of Ed & Fred", publisher=p) + self.assertEqual(p.books.count(), 1) + name = "The Book of Django" + book, created = p.books.update_or_create(defaults={'name': name}, id=book.id) + self.assertFalse(created) + self.assertEqual(book.name, name) + self.assertEqual(p.books.count(), 1) + + def test_create_with_many(self): + """ + Should be able to use update_or_create from the m2m related manager to + create a book. Refs #23611. + """ + p = Publisher.objects.create(name="Acme Publishing") + author = Author.objects.create(name="Ted") + book, created = author.books.update_or_create(name="The Book of Ed & Fred", publisher=p) + self.assertTrue(created) + self.assertEqual(author.books.count(), 1) + + def test_update_with_many(self): + """ + Should be able to use update_or_create from the m2m related manager to + update a book. Refs #23611. + """ + p = Publisher.objects.create(name="Acme Publishing") + author = Author.objects.create(name="Ted") + book = Book.objects.create(name="The Book of Ed & Fred", publisher=p) + book.authors.add(author) + self.assertEqual(author.books.count(), 1) + name = "The Book of Django" + book, created = author.books.update_or_create(defaults={'name': name}, id=book.id) + self.assertFalse(created) + self.assertEqual(book.name, name) + self.assertEqual(author.books.count(), 1)