From 6354ed892b57886c51e4a032e375cc77d182e68c Mon Sep 17 00:00:00 2001 From: plebbimon Date: Mon, 17 Jul 2023 22:59:35 +0200 Subject: [PATCH 01/14] chore(docs): added example for m2m `through` fields --- docs/guide/nested-fields.rst | 55 ++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/docs/guide/nested-fields.rst b/docs/guide/nested-fields.rst index 4b0b7df..a469fbf 100644 --- a/docs/guide/nested-fields.rst +++ b/docs/guide/nested-fields.rst @@ -283,6 +283,61 @@ populator of the relationship. If you don't have an existing type for creating a user, e.g. the "CreateCatInput" we used above, you can set the type to "auto", which will create a new type. +Many to many with `through` models +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Using `graphene-django-cud`, a `through` model is more akin to a many to one relationship, as it is +an intermediate model that connects two other models with itself. + +If you have a many to many relationship with a `through` model, you can use +a `many_to_one_extras` field to specify how to handle the `through` model. +There's also usually no need to specify the `many_to_many_extras` field, as +the `through` model will be used instead. + +Suppose we have a `Dog` model with a many to many relationship +with a `Cat` model, but we want to keep track of the number of times a dog +has fought a cat. We can do this with a `through` model: + +.. code:: python + + class Dog(models.Model): + owner = models.ForeignKey(User, null=True, on_delete=models.SET_NULL) + name = models.TextField() + fights = models.ManyToManyField(Cat, through='Fight') + + class Fight(models.Model): + dog = models.ForeignKey(Dog, on_delete=models.CASCADE, related_name='fights') + cat = models.ForeignKey(Cat, on_delete=models.CASCADE, related_name='fights') + times = models.IntegerField(default=0) + +We can then create a mutation to create a dog, and add a fight to it: + +.. code:: python + + class CreateDogMutation(DjangoCreateMutation): + class Meta: + model = Dog + many_to_one_extras = { + 'fights': { + 'exact': {"type": "auto"} + } + } + +This will infer the dog's ID, and allows us to create a fight in the same +mutation: + +.. code:: + + mutation { + createDog(input: { + name: "Buster", + fights: [{cat: "Q2F0Tm9kZTox", times: 1}] + }}){ + dog{ + ...DogInfo + } + } + } + One to one extras ~~~~~~~~~~~~~~~~~ From 8a520ab1c2b170dd78d164b7eba435515323781c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 Nov 2023 22:13:48 +0000 Subject: [PATCH 02/14] chore(deps-dev): Bump django from 3.2.20 to 3.2.23 Bumps [django](https://github.com/django/django) from 3.2.20 to 3.2.23. - [Commits](https://github.com/django/django/compare/3.2.20...3.2.23) --- updated-dependencies: - dependency-name: django dependency-type: direct:development ... Signed-off-by: dependabot[bot] --- poetry.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/poetry.lock b/poetry.lock index dec2703..95fa6b3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. [[package]] name = "addict" @@ -130,13 +130,13 @@ toml = ["tomli"] [[package]] name = "django" -version = "3.2.20" +version = "3.2.23" description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." optional = false python-versions = ">=3.6" files = [ - {file = "Django-3.2.20-py3-none-any.whl", hash = "sha256:a477ab326ae7d8807dc25c186b951ab8c7648a3a23f9497763c37307a2b5ef87"}, - {file = "Django-3.2.20.tar.gz", hash = "sha256:dec2a116787b8e14962014bf78e120bba454135108e1af9e9b91ade7b2964c40"}, + {file = "Django-3.2.23-py3-none-any.whl", hash = "sha256:d48608d5f62f2c1e260986835db089fa3b79d6f58510881d316b8d88345ae6e1"}, + {file = "Django-3.2.23.tar.gz", hash = "sha256:82968f3640e29ef4a773af2c28448f5f7a08d001c6ac05b32d02aeee6509508b"}, ] [package.dependencies] From b3d818d4fb7a990f7a6f71658f800c98696e7bea Mon Sep 17 00:00:00 2001 From: M69k65y Date: Wed, 10 Jan 2024 14:06:54 +0300 Subject: [PATCH 03/14] Fix disambiguate_id to correctly handle UUIDs passed as strings - Update disambiguate_id to check if result of from_global_id has a value. - Add Fish model with explicit UUID PK field. - Add tests to mimic UUIDs passed as a string. --- graphene_django_cud/tests/factories.py | 16 +++++- .../migrations/0009_auto_20231228_1921.py | 26 ++++++++++ graphene_django_cud/tests/models.py | 6 +++ graphene_django_cud/tests/schema.py | 28 +++++++++++ .../tests/test_create_mutation.py | 47 ++++++++++++++++- .../tests/test_delete_mutation.py | 41 ++++++++++++++- .../tests/test_update_mutation.py | 50 ++++++++++++++++++- graphene_django_cud/util/model.py | 5 +- 8 files changed, 214 insertions(+), 5 deletions(-) create mode 100644 graphene_django_cud/tests/migrations/0009_auto_20231228_1921.py diff --git a/graphene_django_cud/tests/factories.py b/graphene_django_cud/tests/factories.py index 2d0d177..9662775 100644 --- a/graphene_django_cud/tests/factories.py +++ b/graphene_django_cud/tests/factories.py @@ -5,7 +5,14 @@ from django.db import models from factory.django import DjangoModelFactory -from graphene_django_cud.tests.models import User, Cat, Dog, Mouse, DogRegistration +from graphene_django_cud.tests.models import ( + User, + Cat, + Dog, + Mouse, + DogRegistration, + Fish, +) class UserFactory(DjangoModelFactory): @@ -104,3 +111,10 @@ class Meta: model = Mouse name = "mouse" + + +class FishFactory(DjangoModelFactory): + class Meta: + model = Fish + + name = "Koi" diff --git a/graphene_django_cud/tests/migrations/0009_auto_20231228_1921.py b/graphene_django_cud/tests/migrations/0009_auto_20231228_1921.py new file mode 100644 index 0000000..ac4ff3b --- /dev/null +++ b/graphene_django_cud/tests/migrations/0009_auto_20231228_1921.py @@ -0,0 +1,26 @@ +# Generated by Django 3.2.20 on 2023-12-28 19:21 + +from django.db import migrations, models +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('tests', '0008_auto_20220313_1242'), + ] + + operations = [ + migrations.CreateModel( + name='Fish', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('name', models.CharField(max_length=40)), + ], + ), + migrations.AlterField( + model_name='user', + name='first_name', + field=models.CharField(blank=True, max_length=150, verbose_name='first name'), + ), + ] diff --git a/graphene_django_cud/tests/models.py b/graphene_django_cud/tests/models.py index 5d5ca47..e0476f1 100644 --- a/graphene_django_cud/tests/models.py +++ b/graphene_django_cud/tests/models.py @@ -1,3 +1,4 @@ +import uuid from django.contrib.auth.models import AbstractUser from django.db import models @@ -59,3 +60,8 @@ class CatUserRelation(models.Model): class Meta: unique_together = (("cat", "user"),) + + +class Fish(models.Model): + id = models.UUIDField(primary_key=True, default=uuid.uuid4) + name = models.CharField(max_length=40, blank=False, null=False) diff --git a/graphene_django_cud/tests/schema.py b/graphene_django_cud/tests/schema.py index b3e0430..9ea013c 100644 --- a/graphene_django_cud/tests/schema.py +++ b/graphene_django_cud/tests/schema.py @@ -18,6 +18,7 @@ Mouse, DogRegistration, CatUserRelation, + Fish, ) @@ -57,18 +58,26 @@ class Meta: interfaces = (Node,) +class FishNode(DjangoObjectType): + class Meta: + model = Fish + interfaces = (Node,) + + class Query(graphene.ObjectType): user = Node.Field(UserNode) cat = Node.Field(CatNode) dog = Node.Field(DogNode) mice = Node.Field(MouseNode) cat_user_relation = Node.Field(CatUserRelationNode) + fish = Node.Field(FishNode) all_users = DjangoConnectionField(UserNode) all_cats = DjangoConnectionField(CatNode) all_dogs = DjangoConnectionField(DogNode) all_mice = DjangoConnectionField(MouseNode) all_cat_user_relations = DjangoConnectionField(CatUserRelationNode) + all_fish = DjangoConnectionField(FishNode) class CreateUserMutation(DjangoCreateMutation): @@ -251,6 +260,21 @@ class Meta: filter_fields = ("name", "name__startswith") +class CreateFishMutation(DjangoCreateMutation): + class Meta: + model = Fish + + +class UpdateFishMutation(DjangoUpdateMutation): + class Meta: + model = Fish + + +class DeleteFishMutation(DjangoDeleteMutation): + class Meta: + model = Fish + + class Mutations(graphene.ObjectType): create_user = CreateUserMutation.Field() @@ -282,5 +306,9 @@ class Mutations(graphene.ObjectType): delete_mouse = DeleteMouseMutation.Field() batch_delete_mouse = FilterDeleteMouseMutation.Field() + create_fish = CreateFishMutation.Field() + update_fish = UpdateFishMutation.Field() + delete_fish = DeleteFishMutation.Field(0) + schema = Schema(query=Query, mutation=Mutations) diff --git a/graphene_django_cud/tests/test_create_mutation.py b/graphene_django_cud/tests/test_create_mutation.py index 6b42941..0cb5795 100644 --- a/graphene_django_cud/tests/test_create_mutation.py +++ b/graphene_django_cud/tests/test_create_mutation.py @@ -10,9 +10,10 @@ UserFactory, CatFactory, DogFactory, + FishFactory, ) from graphene_django_cud.tests.dummy_query import DummyQuery -from graphene_django_cud.tests.models import User, Cat, Dog, DogRegistration +from graphene_django_cud.tests.models import User, Cat, Dog, DogRegistration, Fish from graphene_django_cud.util import disambiguate_id @@ -736,3 +737,47 @@ class Mutations(graphene.ObjectType): cat["catUserRelations"]["edges"][0]["node"]["user"]["id"], to_global_id("UserNode", other_user.id), ) + + +class TestCreateUuidPk(TestCase): + def test__creating_a_record_with_uuid_pk(self): + # This register the FishNode type + from .schema import FishNode # noqa: F401 + + class CreateFishMutation(DjangoCreateMutation): + class Meta: + model = Fish + + class Mutations(graphene.ObjectType): + create_fish = CreateFishMutation.Field() + + user = UserFactory.create() + fish = FishFactory.build() + + schema = Schema(query=DummyQuery, mutation=Mutations) + mutation = """ + mutation CreateFish( + $input: CreateFishInput! + ){ + createFish(input: $input) { + fish { + id + name + } + } + } + """ + + result = schema.execute( + mutation, + variables={ + "input": { + "name": fish.name + } + }, + context=Dict(user=user), + ) + self.assertIsNone(result.errors) + + data = Dict(result.data) + self.assertEqual(data.createFish.fish.name, fish.name) diff --git a/graphene_django_cud/tests/test_delete_mutation.py b/graphene_django_cud/tests/test_delete_mutation.py index ad5d04f..1ca2347 100644 --- a/graphene_django_cud/tests/test_delete_mutation.py +++ b/graphene_django_cud/tests/test_delete_mutation.py @@ -9,9 +9,10 @@ UserWithPermissionsFactory, CatFactory, UserFactory, + FishFactory, ) from graphene_django_cud.tests.dummy_query import DummyQuery -from graphene_django_cud.tests.models import Cat +from graphene_django_cud.tests.models import Cat, Fish from graphene_django_cud.util import disambiguate_id @@ -128,3 +129,41 @@ class Mutations(graphene.ObjectType): ) self.assertIsNotNone(result.errors) self.assertIn("Not permitted", str(result.errors)) + + def test__deleting_a_record_with_uuid_pk__with_pk_as_str(self): + # This register the FishNode type + from .schema import FishNode # noqa: F401 + + class DeleteFishMutation(DjangoDeleteMutation): + class Meta: + model = Fish + + class Mutations(graphene.ObjectType): + delete_fish = DeleteFishMutation.Field() + + user = UserFactory.create() + fish = FishFactory.create() + + schema = Schema(query=DummyQuery, mutation=Mutations) + mutation = """ + mutation DeleteFish( + $id: ID! + ){ + deleteFish(id: $id) { + found + deletedId + } + } + """ + + # Excluded use of `to_global_id` and cast UUID to str to match some + # real-world mutation scenarios. + result = schema.execute( + mutation, + variables={ + "id": str(fish.id) + }, + context=Dict(user=user), + ) + self.assertIsNone(result.errors) + self.assertEqual(Fish.objects.count(), 0) diff --git a/graphene_django_cud/tests/test_update_mutation.py b/graphene_django_cud/tests/test_update_mutation.py index 9c599d7..46cc0bc 100644 --- a/graphene_django_cud/tests/test_update_mutation.py +++ b/graphene_django_cud/tests/test_update_mutation.py @@ -11,8 +11,9 @@ UserWithPermissionsFactory, DogFactory, MouseFactory, + FishFactory, ) -from graphene_django_cud.tests.models import User, Cat, Dog +from graphene_django_cud.tests.models import User, Cat, Dog, Fish from graphene_django_cud.util import disambiguate_id from graphene_django_cud.tests.dummy_query import DummyQuery @@ -484,6 +485,53 @@ class Mutations(graphene.ObjectType): ) self.assertIsNone(result.errors) + def test__updating_a_record_with_uuid_pk__with_pk_as_str(self): + # This register the FishNode type + from .schema import FishNode # noqa: F401 + + class UpdateFishMutation(DjangoUpdateMutation): + class Meta: + model = Fish + + class Mutations(graphene.ObjectType): + update_fish = UpdateFishMutation.Field() + + user = UserFactory.create() + fish = FishFactory.create() + + schema = Schema(query=DummyQuery, mutation=Mutations) + mutation = """ + mutation UpdateFish( + $id: ID! + $input: UpdateFishInput! + ){ + updateFish(id: $id, input: $input) { + fish { + id + name + } + } + } + """ + + # Excluded use of `to_global_id` and cast UUID to str to match some + # real-world mutation scenarios. + result = schema.execute( + mutation, + variables={ + "id": str(fish.id), + "input": { + "name": "Fugu" + } + }, + context=Dict(user=user), + ) + self.assertIsNone(result.errors) + + data = Dict(result.data) + self.assertNotEqual(data.updateFish.fish.name, fish.name) + self.assertEqual(data.updateFish.fish.name, "Fugu") + class TestUpdateMutationManyToManyOnReverseField(TestCase): def test_default_setup__adding_resource_by_id__adds_resource(self): diff --git a/graphene_django_cud/util/model.py b/graphene_django_cud/util/model.py index d149c1a..7ce614f 100644 --- a/graphene_django_cud/util/model.py +++ b/graphene_django_cud/util/model.py @@ -42,7 +42,10 @@ def disambiguate_id(ambiguous_id: Union[int, float, str, uuid.UUID]): if isinstance(ambiguous_id, str): try: - return from_global_id(ambiguous_id)[1] + _id = from_global_id(ambiguous_id)[1] + + if _id: + return _id except (ValueError, TypeError, binascii.Error): pass From 9e07fd090813135020ab322fa52238aa2be828b2 Mon Sep 17 00:00:00 2001 From: Plebbimon Date: Mon, 15 Jan 2024 18:22:16 +0100 Subject: [PATCH 04/14] chore(feedback): minor edits to explain `through` in Django --- docs/guide/nested-fields.rst | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/guide/nested-fields.rst b/docs/guide/nested-fields.rst index a469fbf..364a3ed 100644 --- a/docs/guide/nested-fields.rst +++ b/docs/guide/nested-fields.rst @@ -285,13 +285,14 @@ you can set the type to "auto", which will create a new type. Many to many with `through` models ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Using `graphene-django-cud`, a `through` model is more akin to a many to one relationship, as it is -an intermediate model that connects two other models with itself. +The way Django handles a `through` model internally is functionally similar to using many to one relationships. This +implies that the more convienient option is to use `many_to_one_extras` instead of `many_to_many_extras` when dealing +with `through` models. If you have a many to many relationship with a `through` model, you can use a `many_to_one_extras` field to specify how to handle the `through` model. -There's also usually no need to specify the `many_to_many_extras` field, as -the `through` model will be used instead. +Due to how the m2m fields are automatically generated using the input schema, it is recommended to use the +`many_to_one_extras` field instead of the `many_to_many_extras` field. Suppose we have a `Dog` model with a many to many relationship with a `Cat` model, but we want to keep track of the number of times a dog From 8304b8720aee76671c23775e1fba1577c89d495b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 7 Feb 2024 22:56:02 +0000 Subject: [PATCH 05/14] chore(deps-dev): Bump django from 3.2.23 to 3.2.24 Bumps [django](https://github.com/django/django) from 3.2.23 to 3.2.24. - [Commits](https://github.com/django/django/compare/3.2.23...3.2.24) --- updated-dependencies: - dependency-name: django dependency-type: direct:development ... Signed-off-by: dependabot[bot] --- poetry.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/poetry.lock b/poetry.lock index 95fa6b3..1fab1df 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "addict" @@ -130,13 +130,13 @@ toml = ["tomli"] [[package]] name = "django" -version = "3.2.23" +version = "3.2.24" description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." optional = false python-versions = ">=3.6" files = [ - {file = "Django-3.2.23-py3-none-any.whl", hash = "sha256:d48608d5f62f2c1e260986835db089fa3b79d6f58510881d316b8d88345ae6e1"}, - {file = "Django-3.2.23.tar.gz", hash = "sha256:82968f3640e29ef4a773af2c28448f5f7a08d001c6ac05b32d02aeee6509508b"}, + {file = "Django-3.2.24-py3-none-any.whl", hash = "sha256:5dd5b787c3ba39637610fe700f54bf158e33560ea0dba600c19921e7ff926ec5"}, + {file = "Django-3.2.24.tar.gz", hash = "sha256:aaee9fb0fb4ebd4311520887ad2e33313d368846607f82a9a0ed461cd4c35b18"}, ] [package.dependencies] From 53e0b907bba20ccb73ed904947cad36e6da59358 Mon Sep 17 00:00:00 2001 From: M69k65y Date: Tue, 20 Feb 2024 16:19:04 +0300 Subject: [PATCH 06/14] Change flake8 repository URL in pre-commit-config --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 55b10e2..eb1febf 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,7 +12,7 @@ repos: exclude: (migrations/|locale/|docs/) args: - --line-length=120 -- repo: https://gitlab.com/pycqa/flake8 +- repo: https://github.com/pycqa/flake8 rev: 3.9.2 hooks: - id: flake8 From 6d7e14e7e80210fade877767a2a069e7623f98de Mon Sep 17 00:00:00 2001 From: M69k65y Date: Wed, 21 Feb 2024 16:31:13 +0300 Subject: [PATCH 07/14] Remove unused imports in converter.py and skip check in batch_delete.py --- graphene_django_cud/converter.py | 2 -- graphene_django_cud/mutations/batch_delete.py | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/graphene_django_cud/converter.py b/graphene_django_cud/converter.py index ed5df0b..f97003a 100644 --- a/graphene_django_cud/converter.py +++ b/graphene_django_cud/converter.py @@ -8,7 +8,6 @@ # # From the last point, users of this module are expected to discard any field returning None from functools import singledispatch -from typing import Union import graphql from django.db import models @@ -31,7 +30,6 @@ Dynamic, Decimal, ) -from graphene.types import enum from graphene.types.json import JSONString from graphene_django.compat import ArrayField, HStoreField, RangeField from graphene_file_upload.scalars import Upload diff --git a/graphene_django_cud/mutations/batch_delete.py b/graphene_django_cud/mutations/batch_delete.py index 8eed8c6..8a42d4c 100644 --- a/graphene_django_cud/mutations/batch_delete.py +++ b/graphene_django_cud/mutations/batch_delete.py @@ -109,7 +109,7 @@ def mutate(cls, root, info, ids): cls.check_permissions(root, info, ids) - Model = cls._meta.model + Model = cls._meta.model # noqa ids = cls.resolve_ids(ids) cls.validate(root, info, ids) From 3d1e6f481a349d88c25c4ae5a4c8e2228b329168 Mon Sep 17 00:00:00 2001 From: M69k65y Date: Wed, 21 Feb 2024 16:39:23 +0300 Subject: [PATCH 08/14] Change param indentation in convert_django_field_with_choices --- graphene_django_cud/converter.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/graphene_django_cud/converter.py b/graphene_django_cud/converter.py index f97003a..8a6b785 100644 --- a/graphene_django_cud/converter.py +++ b/graphene_django_cud/converter.py @@ -117,12 +117,12 @@ def description(self): def convert_django_field_with_choices( - field, - registry=None, - required=None, - field_many_to_many_extras=None, - field_foreign_key_extras=None, - field_one_to_one_extras=None, + field, + registry=None, + required=None, + field_many_to_many_extras=None, + field_foreign_key_extras=None, + field_one_to_one_extras=None, ): choices = getattr(field, "choices", None) From f1a120bc25ac6da73da79400bf11dd927a9f8e98 Mon Sep 17 00:00:00 2001 From: M69k65y Date: Wed, 21 Feb 2024 16:59:56 +0300 Subject: [PATCH 09/14] Format files --- graphene_django_cud/converter.py | 236 +++++++++--------- graphene_django_cud/mutations/batch_create.py | 2 +- graphene_django_cud/mutations/batch_delete.py | 3 +- graphene_django_cud/mutations/batch_patch.py | 4 +- graphene_django_cud/mutations/batch_update.py | 2 +- graphene_django_cud/mutations/create.py | 2 +- .../mutations/filter_update.py | 4 +- graphene_django_cud/mutations/patch.py | 4 +- .../tests/test_create_mutation.py | 8 +- .../tests/test_delete_mutation.py | 4 +- .../tests/test_update_mutation.py | 7 +- 11 files changed, 133 insertions(+), 143 deletions(-) diff --git a/graphene_django_cud/converter.py b/graphene_django_cud/converter.py index 8a6b785..7fa1fbc 100644 --- a/graphene_django_cud/converter.py +++ b/graphene_django_cud/converter.py @@ -158,9 +158,11 @@ def convert_django_field_with_choices( else: # Fetch the actual converted Choices class. We have to do this with a slightly shady usage of # the protected "_of_type" property of the NonNull type. - UnderlyingEnumCls = existing_conversion_in_registry.type._of_type if isinstance( # noqa - existing_conversion_in_registry.type, - NonNull) else existing_conversion_in_registry.type + UnderlyingEnumCls = ( + existing_conversion_in_registry.type._of_type + if isinstance(existing_conversion_in_registry.type, NonNull) # noqa + else existing_conversion_in_registry.type + ) # Return the converted field with the correct description and required value. return UnderlyingEnumCls(description=field.help_text, required=is_required(field, required)) @@ -177,12 +179,12 @@ def convert_django_field_with_choices( @singledispatch def convert_django_field_to_input( - field, - registry=None, - required=None, - field_many_to_many_extras=None, - field_foreign_key_extras=None, - field_one_to_one_extras=None, + field, + registry=None, + required=None, + field_many_to_many_extras=None, + field_foreign_key_extras=None, + field_one_to_one_extras=None, ): raise Exception("Don't know how to convert the Django field %s (%s)" % (field, field.__class__)) @@ -195,12 +197,12 @@ def convert_django_field_to_input( @convert_django_field_to_input.register(models.GenericIPAddressField) @convert_django_field_to_input.register(models.FilePathField) def convert_field_to_string_extended( - field, - registry=None, - required=None, - field_many_to_many_extras=None, - field_foreign_key_extras=None, - field_one_to_one_extras=None, + field, + registry=None, + required=None, + field_many_to_many_extras=None, + field_foreign_key_extras=None, + field_one_to_one_extras=None, ): return String(description=field.help_text, required=is_required(field, required)) @@ -208,12 +210,12 @@ def convert_field_to_string_extended( @convert_django_field_to_input.register(models.OneToOneField) @convert_django_field_to_input.register(models.OneToOneRel) def convert_one_to_one_field( - field, - registry=None, - required=None, - field_many_to_many_extras=None, - field_foreign_key_extras=None, - field_one_to_one_extras=None, + field, + registry=None, + required=None, + field_many_to_many_extras=None, + field_foreign_key_extras=None, + field_one_to_one_extras=None, ): type_name = field_one_to_one_extras.get("type", "ID") if field_one_to_one_extras else "ID" if type_name == "ID": @@ -240,12 +242,12 @@ def dynamic_type(): @convert_django_field_to_input.register(models.AutoField) @convert_django_field_to_input.register(models.ForeignKey) def convert_field_to_id( - field, - registry=None, - required=None, - field_many_to_many_extras=None, - field_foreign_key_extras=None, - field_one_to_one_extras=None, + field, + registry=None, + required=None, + field_many_to_many_extras=None, + field_foreign_key_extras=None, + field_one_to_one_extras=None, ): # Call getattr here, as OneToOneRel does not carry the attribute whatsoeever. id_type = ID( @@ -272,12 +274,12 @@ def dynamic_type(): @convert_django_field_to_input.register(models.UUIDField) def convert_field_to_uuid( - field, - registry=None, - required=None, - field_many_to_many_extras=None, - field_foreign_key_extras=None, - field_one_to_one_extras=None, + field, + registry=None, + required=None, + field_many_to_many_extras=None, + field_foreign_key_extras=None, + field_one_to_one_extras=None, ): return UUID(description=field.help_text, required=is_required(field, required)) @@ -288,24 +290,24 @@ def convert_field_to_uuid( @convert_django_field_to_input.register(models.BigIntegerField) @convert_django_field_to_input.register(models.IntegerField) def convert_field_to_int( - field, - registry=None, - required=None, - field_many_to_many_extras=None, - field_foreign_key_extras=None, - field_one_to_one_extras=None, + field, + registry=None, + required=None, + field_many_to_many_extras=None, + field_foreign_key_extras=None, + field_one_to_one_extras=None, ): return Int(description=field.help_text, required=is_required(field, required)) @convert_django_field_to_input.register(models.BooleanField) def convert_field_to_boolean( - field, - registry=None, - required=None, - field_many_to_many_extras=None, - field_foreign_key_extras=None, - field_one_to_one_extras=None, + field, + registry=None, + required=None, + field_many_to_many_extras=None, + field_foreign_key_extras=None, + field_one_to_one_extras=None, ): if is_required(field, required): return NonNull(Boolean, description=field.help_text) @@ -316,60 +318,60 @@ def convert_field_to_boolean( @convert_django_field_to_input.register(models.NullBooleanField) def convert_field_to_nullboolean( - field, - registry=None, - required=None, - field_many_to_many_extras=None, - field_foreign_key_extras=None, - field_one_to_one_extras=None, + field, + registry=None, + required=None, + field_many_to_many_extras=None, + field_foreign_key_extras=None, + field_one_to_one_extras=None, ): return Boolean(description=field.help_text, required=is_required(field, required)) @convert_django_field_to_input.register(models.FloatField) def convert_field_to_float( - field, - registry=None, - required=None, - field_many_to_many_extras=None, - field_foreign_key_extras=None, - field_one_to_one_extras=None, + field, + registry=None, + required=None, + field_many_to_many_extras=None, + field_foreign_key_extras=None, + field_one_to_one_extras=None, ): return Float(description=field.help_text, required=is_required(field, required)) @convert_django_field_to_input.register(models.DecimalField) def convert_field_to_decimal( - field, - registry=None, - required=None, - field_many_to_many_extras=None, - field_foreign_key_extras=None, - field_one_to_one_extras=None, + field, + registry=None, + required=None, + field_many_to_many_extras=None, + field_foreign_key_extras=None, + field_one_to_one_extras=None, ): return Decimal(description=field.help_text, required=is_required(field, required)) @convert_django_field_to_input.register(models.DurationField) def convert_field_to_time_delta( - field, - registry=None, - required=None, - field_many_to_many_extras=None, - field_foreign_key_extras=None, - field_one_to_one_extras=None, + field, + registry=None, + required=None, + field_many_to_many_extras=None, + field_foreign_key_extras=None, + field_one_to_one_extras=None, ): return TimeDelta(description=field.help_text, required=is_required(field, required)) @convert_django_field_to_input.register(models.DateTimeField) def convert_datetime_to_string( - field, - registry=None, - required=None, - field_many_to_many_extras=None, - field_foreign_key_extras=None, - field_one_to_one_extras=None, + field, + registry=None, + required=None, + field_many_to_many_extras=None, + field_foreign_key_extras=None, + field_one_to_one_extras=None, ): # We only render DateTimeFields with auto_now[_add] if they are explicitly required or not if required is None and (getattr(field, "auto_now", None) or getattr(field, "auto_now_add", None)): @@ -380,12 +382,12 @@ def convert_datetime_to_string( @convert_django_field_to_input.register(models.DateField) def convert_date_to_string( - field, - registry=None, - required=None, - field_many_to_many_extras=None, - field_foreign_key_extras=None, - field_one_to_one_extras=None, + field, + registry=None, + required=None, + field_many_to_many_extras=None, + field_foreign_key_extras=None, + field_one_to_one_extras=None, ): # We only render DateFields with auto_now[_add] if they are explicitly required or not if required is None and (getattr(field, "auto_now", None) or getattr(field, "auto_now_add", None)): @@ -396,12 +398,12 @@ def convert_date_to_string( @convert_django_field_to_input.register(models.TimeField) def convert_time_to_string( - field, - registry=None, - required=None, - field_many_to_many_extras=None, - field_foreign_key_extras=None, - field_one_to_one_extras=None, + field, + registry=None, + required=None, + field_many_to_many_extras=None, + field_foreign_key_extras=None, + field_one_to_one_extras=None, ): return Time(description=field.help_text, required=is_required(field, required)) @@ -410,12 +412,12 @@ def convert_time_to_string( @convert_django_field_to_input.register(models.ManyToManyRel) @convert_django_field_to_input.register(models.ManyToOneRel) def convert_many_to_many_field( - field, - registry=None, - required=None, - field_many_to_many_extras=None, - field_foreign_key_extras=None, - field_one_to_one_extras=None, + field, + registry=None, + required=None, + field_many_to_many_extras=None, + field_foreign_key_extras=None, + field_one_to_one_extras=None, ): # Use getattr on help_text here as ManyToOnRel does not possess this. list_id_type = List( @@ -446,12 +448,12 @@ def dynamic_type(): @convert_django_field_to_input.register(ArrayField) def convert_postgres_array_to_list( - field, - registry=None, - required=None, - field_many_to_many_extras=None, - field_foreign_key_extras=None, - field_one_to_one_extras=None, + field, + registry=None, + required=None, + field_many_to_many_extras=None, + field_foreign_key_extras=None, + field_one_to_one_extras=None, ): base_type = convert_django_field_to_input(field.base_field) if not isinstance(base_type, (List, NonNull)): @@ -462,24 +464,24 @@ def convert_postgres_array_to_list( @convert_django_field_to_input.register(HStoreField) @convert_django_field_to_input.register(models.JSONField) def convert_posgres_field_to_string( - field, - registry=None, - required=None, - field_many_to_many_extras=None, - field_foreign_key_extras=None, - field_one_to_one_extras=None, + field, + registry=None, + required=None, + field_many_to_many_extras=None, + field_foreign_key_extras=None, + field_one_to_one_extras=None, ): return JSONString(description=field.help_text, required=is_required(field, required)) @convert_django_field_to_input.register(RangeField) def convert_postgres_range_to_string( - field, - registry=None, - required=None, - field_many_to_many_extras=None, - field_foreign_key_extras=None, - field_one_to_one_extras=None, + field, + registry=None, + required=None, + field_many_to_many_extras=None, + field_foreign_key_extras=None, + field_one_to_one_extras=None, ): inner_type = convert_django_field_to_input(field.base_field) if not isinstance(inner_type, (List, NonNull)): @@ -490,11 +492,11 @@ def convert_postgres_range_to_string( @convert_django_field_to_input.register(FileField) @convert_django_field_to_input.register(ImageField) def convert_file_field_to_upload( - field, - registry=None, - required=None, - field_many_to_many_extras=None, - field_foreign_key_extras=None, - field_one_to_one_extras=None, + field, + registry=None, + required=None, + field_many_to_many_extras=None, + field_foreign_key_extras=None, + field_one_to_one_extras=None, ): return Upload(required=is_required(field, required)) diff --git a/graphene_django_cud/mutations/batch_create.py b/graphene_django_cud/mutations/batch_create.py index 4aa7929..8424081 100644 --- a/graphene_django_cud/mutations/batch_create.py +++ b/graphene_django_cud/mutations/batch_create.py @@ -36,7 +36,7 @@ def __init_subclass_with_meta__( exclude_fields=(), # Deprecated in favor of `exclude` optional_fields=(), required_fields=(), - auto_context_fields=None, + auto_context_fields=None, return_field_name=None, many_to_many_extras=None, foreign_key_extras=None, diff --git a/graphene_django_cud/mutations/batch_delete.py b/graphene_django_cud/mutations/batch_delete.py index 8a42d4c..0294fba 100644 --- a/graphene_django_cud/mutations/batch_delete.py +++ b/graphene_django_cud/mutations/batch_delete.py @@ -109,7 +109,7 @@ def mutate(cls, root, info, ids): cls.check_permissions(root, info, ids) - Model = cls._meta.model # noqa + Model = cls._meta.model # noqa ids = cls.resolve_ids(ids) cls.validate(root, info, ids) @@ -126,7 +126,6 @@ def mutate(cls, root, info, ids): all_global_ids = [cls.get_return_id(id) for id in ids] - missed_ids = list(set(all_global_ids).difference(deleted_ids)) deletion_count, _ = qs_to_delete.delete() diff --git a/graphene_django_cud/mutations/batch_patch.py b/graphene_django_cud/mutations/batch_patch.py index f620f32..03894de 100644 --- a/graphene_django_cud/mutations/batch_patch.py +++ b/graphene_django_cud/mutations/batch_patch.py @@ -17,8 +17,8 @@ def __init_subclass_with_meta__( cls, _meta=None, model=None, - optional_fields=None, - required_fields=None, + optional_fields=None, + required_fields=None, type_name=None, **kwargs, ): diff --git a/graphene_django_cud/mutations/batch_update.py b/graphene_django_cud/mutations/batch_update.py index 3a01bef..b3b388c 100644 --- a/graphene_django_cud/mutations/batch_update.py +++ b/graphene_django_cud/mutations/batch_update.py @@ -36,7 +36,7 @@ def __init_subclass_with_meta__( exclude_fields=(), # Deprecated in favor of `exclude` optional_fields=(), required_fields=(), - auto_context_fields=None, + auto_context_fields=None, return_field_name=None, many_to_many_extras=None, foreign_key_extras=None, diff --git a/graphene_django_cud/mutations/create.py b/graphene_django_cud/mutations/create.py index 9e2f6c6..f4898bb 100644 --- a/graphene_django_cud/mutations/create.py +++ b/graphene_django_cud/mutations/create.py @@ -36,7 +36,7 @@ def __init_subclass_with_meta__( exclude_fields=(), # Deprecated in favor of `exclude` optional_fields=(), required_fields=(), - auto_context_fields=None, + auto_context_fields=None, return_field_name=None, many_to_many_extras=None, foreign_key_extras=None, diff --git a/graphene_django_cud/mutations/filter_update.py b/graphene_django_cud/mutations/filter_update.py index f5326ac..9fc7897 100644 --- a/graphene_django_cud/mutations/filter_update.py +++ b/graphene_django_cud/mutations/filter_update.py @@ -45,10 +45,10 @@ def __init_subclass_with_meta__( only_fields=(), # Deprecated in favor of `fields` exclude=(), exclude_fields=(), # Deprecated in favor of `exclude` - optional_fields=None, # Explicitly defaulted to None here and handled below. + optional_fields=None, # Explicitly defaulted to None here and handled below. required_fields=(), field_types=None, - auto_context_fields=None, + auto_context_fields=None, **kwargs, ): diff --git a/graphene_django_cud/mutations/patch.py b/graphene_django_cud/mutations/patch.py index a51db20..ccce273 100644 --- a/graphene_django_cud/mutations/patch.py +++ b/graphene_django_cud/mutations/patch.py @@ -17,8 +17,8 @@ def __init_subclass_with_meta__( cls, _meta=None, model=None, - optional_fields=None, - required_fields=None, + optional_fields=None, + required_fields=None, type_name=None, **kwargs, ): diff --git a/graphene_django_cud/tests/test_create_mutation.py b/graphene_django_cud/tests/test_create_mutation.py index 0cb5795..7a4acae 100644 --- a/graphene_django_cud/tests/test_create_mutation.py +++ b/graphene_django_cud/tests/test_create_mutation.py @@ -750,7 +750,7 @@ class Meta: class Mutations(graphene.ObjectType): create_fish = CreateFishMutation.Field() - + user = UserFactory.create() fish = FishFactory.build() @@ -770,11 +770,7 @@ class Mutations(graphene.ObjectType): result = schema.execute( mutation, - variables={ - "input": { - "name": fish.name - } - }, + variables={"input": {"name": fish.name}}, context=Dict(user=user), ) self.assertIsNone(result.errors) diff --git a/graphene_django_cud/tests/test_delete_mutation.py b/graphene_django_cud/tests/test_delete_mutation.py index 1ca2347..213d4ac 100644 --- a/graphene_django_cud/tests/test_delete_mutation.py +++ b/graphene_django_cud/tests/test_delete_mutation.py @@ -160,9 +160,7 @@ class Mutations(graphene.ObjectType): # real-world mutation scenarios. result = schema.execute( mutation, - variables={ - "id": str(fish.id) - }, + variables={"id": str(fish.id)}, context=Dict(user=user), ) self.assertIsNone(result.errors) diff --git a/graphene_django_cud/tests/test_update_mutation.py b/graphene_django_cud/tests/test_update_mutation.py index 46cc0bc..685d268 100644 --- a/graphene_django_cud/tests/test_update_mutation.py +++ b/graphene_django_cud/tests/test_update_mutation.py @@ -518,12 +518,7 @@ class Mutations(graphene.ObjectType): # real-world mutation scenarios. result = schema.execute( mutation, - variables={ - "id": str(fish.id), - "input": { - "name": "Fugu" - } - }, + variables={"id": str(fish.id), "input": {"name": "Fugu"}}, context=Dict(user=user), ) self.assertIsNone(result.errors) From 57dede3bf15c170cd13e3826a42ec736aa845c63 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Mar 2024 21:51:34 +0000 Subject: [PATCH 10/14] chore(deps-dev): Bump django from 3.2.24 to 3.2.25 Bumps [django](https://github.com/django/django) from 3.2.24 to 3.2.25. - [Commits](https://github.com/django/django/compare/3.2.24...3.2.25) --- updated-dependencies: - dependency-name: django dependency-type: direct:development ... Signed-off-by: dependabot[bot] --- poetry.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index 1fab1df..0bbb30d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -130,13 +130,13 @@ toml = ["tomli"] [[package]] name = "django" -version = "3.2.24" +version = "3.2.25" description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." optional = false python-versions = ">=3.6" files = [ - {file = "Django-3.2.24-py3-none-any.whl", hash = "sha256:5dd5b787c3ba39637610fe700f54bf158e33560ea0dba600c19921e7ff926ec5"}, - {file = "Django-3.2.24.tar.gz", hash = "sha256:aaee9fb0fb4ebd4311520887ad2e33313d368846607f82a9a0ed461cd4c35b18"}, + {file = "Django-3.2.25-py3-none-any.whl", hash = "sha256:a52ea7fcf280b16f7b739cec38fa6d3f8953a5456986944c3ca97e79882b4e38"}, + {file = "Django-3.2.25.tar.gz", hash = "sha256:7ca38a78654aee72378594d63e51636c04b8e28574f5505dff630895b5472777"}, ] [package.dependencies] From 2152f123cbd534413d5790ac449a6571963753ed Mon Sep 17 00:00:00 2001 From: Jonathan Weth Date: Wed, 17 Apr 2024 18:30:27 +0200 Subject: [PATCH 11/14] Add before_create_obj hook to BatchCreateMutation --- graphene_django_cud/mutations/core.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/graphene_django_cud/mutations/core.py b/graphene_django_cud/mutations/core.py index c27444f..1178dfb 100644 --- a/graphene_django_cud/mutations/core.py +++ b/graphene_django_cud/mutations/core.py @@ -378,7 +378,11 @@ def create_obj( model_field_values[name + "_id"] = obj_id # Foreign keys are added, we are ready to create our object - obj = Model.objects.create(**model_field_values) + obj = Model(**model_field_values) + + cls.before_create_obj(info, input, obj) + + obj.save() # Handle one to one rels if len(one_to_one_rels) > 0: @@ -408,8 +412,6 @@ def create_obj( setattr(obj, name, new_value) - obj.save() - # Handle extras fields for name, extras in many_to_many_extras.items(): field = Model._meta.get_field(name) @@ -757,6 +759,10 @@ def before_save(cls, root, info, *args, **kwargs): def after_mutate(cls, root, info, *args, **kwargs): return None + @classmethod + def before_create_obj(cls, info, input, obj): + return None + @classmethod def resolve_id(cls, id): return disambiguate_id(id) From 94bb94057c629a9d5f9e05884ad5134f5ae96f56 Mon Sep 17 00:00:00 2001 From: Tormod Haugland Date: Wed, 3 Jul 2024 07:48:45 +0200 Subject: [PATCH 12/14] Add a missing obj.save() Signed-off-by: Tormod Haugland --- graphene_django_cud/mutations/core.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/graphene_django_cud/mutations/core.py b/graphene_django_cud/mutations/core.py index 1178dfb..acc8dcb 100644 --- a/graphene_django_cud/mutations/core.py +++ b/graphene_django_cud/mutations/core.py @@ -412,6 +412,11 @@ def create_obj( setattr(obj, name, new_value) + # This is only needed for the case where we are getting an id, and not calling + # the `create_or_update_one_to_one_relation` method. + # Pending a proper code cleanup, this is a temporary fix. + obj.save() + # Handle extras fields for name, extras in many_to_many_extras.items(): field = Model._meta.get_field(name) From 09e5c5be2fbafb3acbe7847fc0424dc195b6d08d Mon Sep 17 00:00:00 2001 From: Tormod Haugland Date: Wed, 3 Jul 2024 07:50:02 +0200 Subject: [PATCH 13/14] Clean up create and update tests - Move an update test to the update file - Add a new create test for one to one relations Signed-off-by: Tormod Haugland --- .../tests/test_create_mutation.py | 117 ++++------------- .../tests/test_update_mutation.py | 122 +++++++++++++++++- 2 files changed, 146 insertions(+), 93 deletions(-) diff --git a/graphene_django_cud/tests/test_create_mutation.py b/graphene_django_cud/tests/test_create_mutation.py index 7a4acae..55fbdb1 100644 --- a/graphene_django_cud/tests/test_create_mutation.py +++ b/graphene_django_cud/tests/test_create_mutation.py @@ -1,18 +1,18 @@ import graphene from addict import Dict from django.test import TestCase -from graphene import Schema from graphene import ResolveInfo +from graphene import Schema from graphql_relay import to_global_id -from graphene_django_cud.mutations import DjangoUpdateMutation, DjangoCreateMutation +from graphene_django_cud.mutations import DjangoCreateMutation +from graphene_django_cud.tests.dummy_query import DummyQuery from graphene_django_cud.tests.factories import ( UserFactory, CatFactory, DogFactory, FishFactory, ) -from graphene_django_cud.tests.dummy_query import DummyQuery from graphene_django_cud.tests.models import User, Cat, Dog, DogRegistration, Fish from graphene_django_cud.util import disambiguate_id @@ -34,7 +34,7 @@ def mock_info(context=None): class TestCreateMutationManyToOneExtras(TestCase): def test_many_to_one_extras__auto_calling_mutation_with_setting_field__does_nothing( - self, + self, ): # This registers the UserNode type from .schema import UserNode # noqa: F401 @@ -230,95 +230,31 @@ class Mutations(graphene.ObjectType): self.assertEqual(user.cats.all().count(), 5) -class TestUpdateWithOneToOneField(TestCase): - def test__one_to_one_relation_exists__updates_specified_fields(self): - +class TestCreateWithOneToOneField(TestCase): + def test__one_to_one__without_extra__assigns_field(self): # This registers the UserNode type - from .schema import UserNode # noqa: F401 + from .schema import UserNode - class UpdateDogMutation(DjangoUpdateMutation): + class CreateDogRegistrationMutation(DjangoCreateMutation): class Meta: - model = Dog - one_to_one_extras = {"registration": {"type": "auto"}} + model = DogRegistration class Mutations(graphene.ObjectType): - update_dog = UpdateDogMutation.Field() + create_dog_registration = CreateDogRegistrationMutation.Field() user = UserFactory.create() dog = DogFactory.create() - DogRegistration.objects.create(dog=dog, registration_number="1234") schema = Schema(query=DummyQuery, mutation=Mutations) - mutation = """ - mutation UpdateDog( - $id: ID!, - $input: UpdateDogInput! - ){ - updateDog(id: $id, input: $input){ - dog{ - id - registration{ - id - registrationNumber - } - } - } - } - """ - result = schema.execute( - mutation, - variables={ - "id": to_global_id("DogNode", dog.id), - "input": { - "name": dog.name, - "breed": dog.breed, - "tag": dog.tag, - "owner": to_global_id("UserNode", dog.owner.id), - "registration": {"registrationNumber": "12345"}, - }, - }, - context=Dict(user=user), - ) - self.assertIsNone(result.errors) - data = Dict(result.data) - self.assertIsNone(result.errors) - self.assertEqual("12345", data.updateDog.dog.registration.registrationNumber) - - # Load from database - dog.refresh_from_db() - self.assertEqual(dog.registration.registration_number, "12345") - - def test__reverse_one_to_one_exists__updates_specified_fields(self): - # This registers the UserNode type - from .schema import UserNode # noqa: F401 - - class UpdateDogRegistrationMutation(DjangoUpdateMutation): - class Meta: - model = DogRegistration - one_to_one_extras = {"dog": {"type": "auto"}} - - class Mutations(graphene.ObjectType): - update_dog_registration = UpdateDogRegistrationMutation.Field() - - user = UserFactory.create() - dog = DogFactory.create(breed="HUSKY") - dog_registration = DogRegistration.objects.create(dog=dog, registration_number="1234") - - schema = Schema(query=DummyQuery, mutation=Mutations) mutation = """ - mutation UpdateDogRegistration( - $id: ID!, - $input: UpdateDogRegistrationInput! + mutation CreateDogRegistration( + $input: CreateDogRegistrationInput! ){ - updateDogRegistration(id: $id, input: $input){ + createDogRegistration(input: $input){ dogRegistration{ id registrationNumber - dog{ - id - breed - } } } } @@ -327,31 +263,27 @@ class Mutations(graphene.ObjectType): result = schema.execute( mutation, variables={ - "id": to_global_id("DogRegistrationNode", dog_registration.id), "input": { - "registrationNumber": dog_registration.registration_number, - "dog": { - "name": dog.name, - "breed": "LABRADOR", - "tag": dog.tag, - "owner": to_global_id("UserNode", dog.owner.id), - }, + "registrationNumber": "12345", + "dog": to_global_id("DogNode", dog.id), }, }, context=Dict(user=user), ) + self.assertIsNone(result.errors) data = Dict(result.data) - self.assertEqual("LABRADOR", data.updateDogRegistration.dogRegistration.dog.breed) - # Load from database - dog_registration.refresh_from_db() - dog.refresh_from_db() + self.assertEqual("12345", data.createDogRegistration.dogRegistration.registrationNumber) - self.assertEqual(dog.breed, "LABRADOR") + dog_registration = DogRegistration.objects.get( + pk=disambiguate_id(data.createDogRegistration.dogRegistration.id)) + self.assertEqual(dog_registration.registration_number, "12345") + dog = getattr(dog_registration, "dog", None) + self.assertIsNotNone(dog) + self.assertEqual(dog.id, dog.id) -class TestCreateWithOneToOneField(TestCase): def test__one_to_one_relation_exists__creates_specified_fields(self): # This registers the UserNode type from .schema import UserNode # noqa: F401 @@ -403,6 +335,7 @@ class Mutations(graphene.ObjectType): # Load from database dog = Dog.objects.get(pk=disambiguate_id(data.createDog.dog.id)) + dog.refresh_from_db() registration = getattr(dog, "registration", None) self.assertIsNotNone(registration) self.assertEqual(registration.registration_number, "12345") @@ -599,7 +532,7 @@ class Mutations(graphene.ObjectType): class TestCreateMutationCustomFields(TestCase): def test_custom_field__separate_from_model_fields__adds_new_field_which_can_be_handled( - self, + self, ): # This registers the UserNode type from .schema import UserNode # noqa: F401 diff --git a/graphene_django_cud/tests/test_update_mutation.py b/graphene_django_cud/tests/test_update_mutation.py index 685d268..39bd770 100644 --- a/graphene_django_cud/tests/test_update_mutation.py +++ b/graphene_django_cud/tests/test_update_mutation.py @@ -13,7 +13,7 @@ MouseFactory, FishFactory, ) -from graphene_django_cud.tests.models import User, Cat, Dog, Fish +from graphene_django_cud.tests.models import User, Cat, Dog, Fish, DogRegistration from graphene_django_cud.util import disambiguate_id from graphene_django_cud.tests.dummy_query import DummyQuery @@ -1616,3 +1616,123 @@ class Mutations(graphene.ObjectType): dog.refresh_from_db() self.assertEqual(1, dog.bark_count) + + +class TestUpdateWithOneToOneField(TestCase): + def test__one_to_one_relation_exists__updates_specified_fields(self): + # This registers the UserNode type + from .schema import UserNode # noqa: F401 + + class UpdateDogMutation(DjangoUpdateMutation): + class Meta: + model = Dog + one_to_one_extras = {"registration": {"type": "auto"}} + + class Mutations(graphene.ObjectType): + update_dog = UpdateDogMutation.Field() + + user = UserFactory.create() + dog = DogFactory.create() + DogRegistration.objects.create(dog=dog, registration_number="1234") + + schema = Schema(query=DummyQuery, mutation=Mutations) + mutation = """ + mutation UpdateDog( + $id: ID!, + $input: UpdateDogInput! + ){ + updateDog(id: $id, input: $input){ + dog{ + id + registration{ + id + registrationNumber + } + } + } + } + """ + + result = schema.execute( + mutation, + variables={ + "id": to_global_id("DogNode", dog.id), + "input": { + "name": dog.name, + "breed": dog.breed, + "tag": dog.tag, + "owner": to_global_id("UserNode", dog.owner.id), + "registration": {"registrationNumber": "12345"}, + }, + }, + context=Dict(user=user), + ) + self.assertIsNone(result.errors) + data = Dict(result.data) + self.assertIsNone(result.errors) + self.assertEqual("12345", data.updateDog.dog.registration.registrationNumber) + + # Load from database + dog.refresh_from_db() + self.assertEqual(dog.registration.registration_number, "12345") + + def test__reverse_one_to_one_exists__updates_specified_fields(self): + # This registers the UserNode type + from .schema import UserNode # noqa: F401 + + class UpdateDogRegistrationMutation(DjangoUpdateMutation): + class Meta: + model = DogRegistration + one_to_one_extras = {"dog": {"type": "auto"}} + + class Mutations(graphene.ObjectType): + update_dog_registration = UpdateDogRegistrationMutation.Field() + + user = UserFactory.create() + dog = DogFactory.create(breed="HUSKY") + dog_registration = DogRegistration.objects.create(dog=dog, registration_number="1234") + + schema = Schema(query=DummyQuery, mutation=Mutations) + mutation = """ + mutation UpdateDogRegistration( + $id: ID!, + $input: UpdateDogRegistrationInput! + ){ + updateDogRegistration(id: $id, input: $input){ + dogRegistration{ + id + registrationNumber + dog{ + id + breed + } + } + } + } + """ + + result = schema.execute( + mutation, + variables={ + "id": to_global_id("DogRegistrationNode", dog_registration.id), + "input": { + "registrationNumber": dog_registration.registration_number, + "dog": { + "name": dog.name, + "breed": "LABRADOR", + "tag": dog.tag, + "owner": to_global_id("UserNode", dog.owner.id), + }, + }, + }, + context=Dict(user=user), + ) + self.assertIsNone(result.errors) + data = Dict(result.data) + self.assertEqual("LABRADOR", data.updateDogRegistration.dogRegistration.dog.breed) + + # Load from database + dog_registration.refresh_from_db() + dog.refresh_from_db() + + self.assertEqual(dog.breed, "LABRADOR") From 683a97c5a39fd2afe49f613ed9c97b5e68068c64 Mon Sep 17 00:00:00 2001 From: Tormod Haugland Date: Wed, 3 Jul 2024 08:04:25 +0200 Subject: [PATCH 14/14] Modernise CI/CD-pipeline - Upgrade to more recent versions of actions - Test python versions 3.9-3.12 - Use ubuntu-latest - Use poetry 1.8.3 Signed-off-by: Tormod Haugland --- .github/workflows/pre-commit.yml | 16 ---------------- .github/workflows/primary.yml | 31 ++++++++++++++----------------- 2 files changed, 14 insertions(+), 33 deletions(-) delete mode 100644 .github/workflows/pre-commit.yml diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml deleted file mode 100644 index dac839b..0000000 --- a/.github/workflows/pre-commit.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: pre-commit - -on: - pull_request: - push: - branches: [ develop, master ] - -jobs: - pre-commit: - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 - with: - python-version: '3.8' - - uses: pre-commit/action@v2.0.3 diff --git a/.github/workflows/primary.yml b/.github/workflows/primary.yml index 0dcf9d0..2252f6e 100644 --- a/.github/workflows/primary.yml +++ b/.github/workflows/primary.yml @@ -10,21 +10,18 @@ on: jobs: test: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.7", "3.8", "3.9", "3.10"] - poetry-version: ["1.3.1"] + python-version: [ "3.9", "3.10", "3.11", "3.12" ] + poetry-version: [ "1.8.3" ] steps: - - uses: actions/checkout@v2 - - name: Run image - uses: abatilo/actions-poetry@v2.2.0 - with: - poetry-version: ${{ matrix.poetry-version }} - + - uses: actions/checkout@v4 + - name: Install Poetry + uses: snok/install-poetry@v1 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} cache: 'poetry' @@ -36,23 +33,23 @@ jobs: release: needs: test if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: - python-version: 3.9 + python-version: 3.12 + - name: Install Poetry + uses: snok/install-poetry@v1 - name: Install dependencies run: | pip install twine - pip install poetry==1.1.4 pip install wheel - poetry config virtualenvs.create false - poetry install --no-interaction + poetry install - name: Build package run: make build - name: Publish package