diff --git a/graphene_django_cud/mutations/batch_create.py b/graphene_django_cud/mutations/batch_create.py index 65390ea..4aa7929 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={}, + 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_patch.py b/graphene_django_cud/mutations/batch_patch.py index 2e4e882..f620f32 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, ): @@ -35,8 +35,8 @@ def __init_subclass_with_meta__( return super().__init_subclass_with_meta__( _meta=_meta, model=model, - optional_fields=optional_fields, - required_fields=required_fields, + optional_fields=optional_fields or (), + required_fields=required_fields or (), type_name=input_type_name, **kwargs, ) diff --git a/graphene_django_cud/mutations/batch_update.py b/graphene_django_cud/mutations/batch_update.py index a9b6d30..3a01bef 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={}, + 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 b6f8753..9e2f6c6 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={}, + 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 24b8826..f5326ac 100644 --- a/graphene_django_cud/mutations/filter_update.py +++ b/graphene_django_cud/mutations/filter_update.py @@ -45,12 +45,13 @@ def __init_subclass_with_meta__( only_fields=(), # Deprecated in favor of `fields` exclude=(), exclude_fields=(), # Deprecated in favor of `exclude` - optional_fields=None, + optional_fields=None, # Explicitly defaulted to None here and handled below. required_fields=(), field_types=None, - auto_context_fields={}, + auto_context_fields=None, **kwargs, ): + registry = get_global_registry() model_type = registry.get_type_for_model(model) @@ -77,6 +78,9 @@ def __init_subclass_with_meta__( DeprecationWarning, ) + if auto_context_fields is None: + auto_context_fields = {} + input_arguments = get_filter_fields_input_args(filter_fields, model) FilterInputType = type( diff --git a/graphene_django_cud/mutations/patch.py b/graphene_django_cud/mutations/patch.py index 8caf892..a51db20 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, ): @@ -35,8 +35,8 @@ def __init_subclass_with_meta__( return super().__init_subclass_with_meta__( _meta=_meta, model=model, - optional_fields=optional_fields, - required_fields=required_fields, + optional_fields=optional_fields or (), + required_fields=required_fields or (), type_name=input_type_name, **kwargs, ) 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 c681a2e..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 @@ -135,6 +138,9 @@ def get_input_fields_for_model( # Or when there is no back reference. continue + optional_fields = optional_fields or () + required_fields = required_fields or () + required = None if name in optional_fields: required = False 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]