def test__many_to_one_relation_exists__creates_specified_fields(self): # This registers the UserNode type # noinspection PyUnresolvedReferences from .schema import UserNode class CreateDogMutation(DjangoCreateMutation): class Meta: model = Dog class Mutations(graphene.ObjectType): create_dog = CreateDogMutation.Field() user = UserFactory.create() cat = CatFactory.create() schema = Schema(mutation=Mutations) mutation = """ mutation CreateDog( $input: CreateDogInput! ){ createDog(input: $input){ dog{ id enemies{ edges{ node{ id } } } } } } """ result = schema.execute( mutation, variables={ "input": { "name": "Sparky", "breed": "HUSKY", "tag": "1234", "owner": to_global_id("UserNode", user.id), "enemies": [to_global_id("CatNode", cat.id)], }, }, context=Dict(user=user), ) self.assertIsNone(result.errors) data = Dict(result.data) self.assertIsNone(result.errors) self.assertEqual(to_global_id("CatNode", cat.id), data.createDog.dog.enemies.edges[0].node.id) new_dog = Dog.objects.get(pk=disambiguate_id(data.createDog.dog.id)) # Load from database cat.refresh_from_db() self.assertEqual(cat, new_dog.enemies.first())
def mutate(cls, root, info, id): cls.before_mutate(root, info, id) if cls._meta.login_required and not info.context.user.is_authenticated: raise GraphQLError("Must be logged in to access this mutation.") cls.validate(root, info, id) Model = cls._meta.model id = disambiguate_id(id) try: obj = cls.get_queryset(root, info, id).get(pk=id) cls.check_permissions(root, info, id, obj) updated_obj = cls.before_save(root, info, id, obj) if updated_obj: obj = updated_obj obj.delete() cls.after_mutate(root, info, id, True) return cls(found=True, deleted_id=id) except ObjectDoesNotExist: cls.after_mutate(root, info, id, False) return cls(found=False)
def get_or_create_m2m_objs(cls, field, values, data, operation, info): results = [] if not values: return results if isinstance(data, bool): data = {} field_type = data.get("type", "ID") for value in values: if field_type == "ID": related_obj = field.related_model.objects.get( pk=disambiguate_id(value)) else: # This is something that we are going to create input_type_meta = meta_registry.get_meta_for_type(field_type) # Create new obj related_obj = cls.create_obj( value, info, input_type_meta.get("auto_context_fields", {}), input_type_meta.get("many_to_many_extras", {}), input_type_meta.get("foreign_key_extras", {}), input_type_meta.get("many_to_one_extras", {}), input_type_meta.get("one_to_one_extras", {}), field.related_model, ) results.append(related_obj) return results
def test__one_to_one_relation_exists__creates_specified_fields(self): # This registers the UserNode type # noinspection PyUnresolvedReferences from .schema import UserNode class CreateDogMutation(DjangoCreateMutation): class Meta: model = Dog one_to_one_extras = {"registration": {"type": "auto"}} class Mutations(graphene.ObjectType): create_dog = CreateDogMutation.Field() user = UserFactory.create() schema = Schema(mutation=Mutations) mutation = """ mutation CreateDog( $input: CreateDogInput! ){ createDog(input: $input){ dog{ id registration{ id registrationNumber } } } } """ result = schema.execute( mutation, variables={ "input": { "name": "Sparky", "breed": "HUSKY", "tag": "1234", "owner": to_global_id("UserNode", user.id), "registration": { "registrationNumber": "12345" }, }, }, context=Dict(user=user), ) self.assertIsNone(result.errors) data = Dict(result.data) self.assertIsNone(result.errors) self.assertEqual("12345", data.createDog.dog.registration.registrationNumber) # Load from database dog = Dog.objects.get(pk=disambiguate_id(data.createDog.dog.id)) registration = getattr(dog, "registration", None) self.assertIsNotNone(registration) self.assertEqual(registration.registration_number, "12345")
def get_or_upsert_m2o_objs(cls, obj, field, values, data, operation, info, Model): results = [] if not values: return results field_type = data.get("type", "auto") for value in values: if field_type == "ID": related_obj = field.related_model.objects.get( pk=disambiguate_id(value)) results.append(related_obj) elif field_type == "auto": # In this case, a new type has been created for us. Let's first find it's name, # then get it's meta, and then create it. We also need to attach the obj as the # foreign key. _type_name = data.get( "type_name", f"{operation.capitalize()}{Model.__name__}{field.name.capitalize()}", ) input_type_meta = meta_registry.get_meta_for_type(field_type) # Ensure the parent relation exists and has the correct id. value[field.field.name] = obj.id # We use upsert here, as the operation might be "update", where we # want to update the object. related_obj = cls.upsert_obj( value, info, input_type_meta.get("auto_context_fields", {}), input_type_meta.get("many_to_many_extras", {}), input_type_meta.get("foreign_key_extras", {}), input_type_meta.get("many_to_one_extras", {}), input_type_meta.get("one_to_one_extras", {}), field.related_model, ) results.append(related_obj) else: # This is something that we are going to create input_type_meta = meta_registry.get_meta_for_type(field_type) # Create new obj related_obj = cls.create_obj( value, info, input_type_meta.get("auto_context_fields", {}), input_type_meta.get("many_to_many_extras", {}), input_type_meta.get("foreign_key_extras", {}), input_type_meta.get("many_to_one_extras", {}), input_type_meta.get("one_to_one_extras", {}), field.related_model, ) results.append(related_obj) return results
def test_many_to_one_extras__add_by_input__adds_by_input(self): # This registers the UserNode type # noinspection PyUnresolvedReferences from .schema import UserNode class CreateCatMutation(DjangoCreateMutation): class Meta: model = Cat class CreateUserMutation(DjangoCreateMutation): class Meta: model = User exclude_fields = ("password", ) many_to_one_extras = {"cats": {"add": {"type": "auto"}}} class Mutations(graphene.ObjectType): create_cat = CreateCatMutation.Field() create_user = CreateUserMutation.Field() user = UserFactory.build() schema = Schema(mutation=Mutations) mutation = """ mutation CreateUser( $input: CreateUserInput! ){ createUser(input: $input){ user{ id } } } """ result = schema.execute(mutation, variables={ "input": { "username": user.username, "firstName": user.first_name, "lastName": user.last_name, "email": user.email, "catsAdd": [{ "name": "Cat Damon" } for _ in range(5)] } }, context=Dict(user=user)) self.assertIsNone(result.errors) data = Dict(result.data) user = User.objects.get(pk=disambiguate_id(data.createUser.user.id)) self.assertEqual(user.cats.all().count(), 5)
def get_all_objs(cls, Model, ids: Iterable[Union[str, int]]): """ Helper method for getting a number of objects with Model.objects.get() :return: """ objs = [] for id in ids: objs.append(Model.objects.get(pk=disambiguate_id(id))) return objs
def mutate(cls, root, info, input, id): updated_input = cls.before_mutate(root, info, input, id) if updated_input: input = updated_input if cls._meta.login_required and not info.context.user.is_authenticated: raise GraphQLError("Must be logged in to access this mutation.") id = disambiguate_id(id) Model = cls._meta.model queryset = cls.get_queryset(root, info, input, id) obj = queryset.get(pk=id) auto_context_fields = cls._meta.auto_context_fields or {} cls.check_permissions(root, info, input, id, obj) cls.validate(root, info, input, id, obj) with transaction.atomic(): obj = cls.update_obj( obj, input, info, auto_context_fields, cls._meta.many_to_many_extras, cls._meta.foreign_key_extras, cls._meta.many_to_one_extras, cls._meta.one_to_one_extras, Model, ) updated_obj = cls.before_save(root, info, input, id, obj) if updated_obj: obj = updated_obj obj.save() return_data = {cls._meta.return_field_name: obj} cls.after_mutate(root, info, obj, return_data) return cls(**return_data)
def test_mutate__object_exists__deletes_object(self): # This registers the UserNode type # noinspection PyUnresolvedReferences from .schema import UserNode class DeleteCatMutation(DjangoDeleteMutation): class Meta: model = Cat permissions = ("tests.delete_cat", ) class Mutations(graphene.ObjectType): delete_cat = DeleteCatMutation.Field() user = UserWithPermissionsFactory.create( permissions=["tests.delete_cat"]) cat = CatFactory.create() schema = Schema(mutation=Mutations) mutation = """ mutation DeleteCat( $id: ID! ){ deleteCat(id: $id){ found deletedId deletedRawId deletedInputId } } """ result = schema.execute( mutation, variables={ "id": to_global_id("CatNode", cat.id), }, context=Dict(user=user), ) self.assertIsNone(result.errors) data = Dict(result.data) self.assertIsNone(Cat.objects.filter(id=cat.id).first()) self.assertTrue(data.deleteCat.found) self.assertEqual(cat.id, int(disambiguate_id(data.deleteCat.deletedId)))
def upsert_obj( cls, input, info, auto_context_fields, many_to_many_extras, foreign_key_extras, many_to_one_extras, one_to_one_extras, Model, ): id = disambiguate_id(input.get("id")) obj = Model.objects.filter(pk=id).first() if obj: obj = cls.update_obj( obj, input, info, auto_context_fields, many_to_many_extras, foreign_key_extras, many_to_one_extras, one_to_one_extras, Model, ) obj.save() return obj else: return cls.create_obj( input, info, auto_context_fields, many_to_many_extras, foreign_key_extras, many_to_one_extras, one_to_one_extras, Model, )
def update_obj( cls, obj, input, info, auto_context_fields, many_to_many_extras, foreign_key_extras, many_to_one_extras, one_to_one_extras, Model, ): many_to_many_to_add = {} many_to_many_to_remove = {} many_to_many_to_set = {} many_to_one_to_add = {} many_to_one_to_remove = {} many_to_one_to_set = {} many_to_many_extras_field_names = get_m2m_all_extras_field_names( many_to_many_extras) many_to_one_extras_field_names = get_m2m_all_extras_field_names( many_to_one_extras) # The layout is the same as for m2m foreign_key_extras_field_names = get_fk_all_extras_field_names( foreign_key_extras) for field_name, context_name in auto_context_fields.items(): if hasattr(info.context, context_name): setattr(obj, field_name, getattr(info.context, context_name)) for name, value in super(type(input), input).items(): # Handle these separately if (name in many_to_many_extras_field_names or name in foreign_key_extras_field_names or name in many_to_one_extras_field_names): continue field = Model._meta.get_field(name) new_value = value # We have to handle this case specifically, by using the fields # .set()-method, instead of direct assignment field_is_many_to_many = is_field_many_to_many(field) field_is_one_to_one = is_field_one_to_one(field) value_handle_name = "handle_" + name if hasattr(cls, value_handle_name): handle_func = getattr(cls, value_handle_name) assert callable( handle_func ), f"Property {value_handle_name} on {cls.__name__} is not a function." new_value = handle_func(value, name, info) # On some fields we perform some default conversion, if the value was not transformed above. if new_value == value and value is not None: if isinstance(field, models.AutoField): new_value = disambiguate_id(value) elif isinstance(field, models.OneToOneField): # If the value is an integer or a string, we assume it is an ID if isinstance(value, str) or isinstance(value, int): name = getattr(field, "db_column", None) or name + "_id" new_value = disambiguate_id(value) else: extra_data = one_to_one_extras.get(name, {}) # This is a nested field we need to take care of. value[field.remote_field.name] = obj.id new_value = cls.create_or_update_one_to_one_relation( obj, field, value, extra_data, info) elif isinstance(field, models.OneToOneRel): # If the value is an integer or a string, we assume it is an ID if isinstance(value, str) or isinstance(value, int): name = getattr(field, "db_column", None) or name + "_id" new_value = disambiguate_id(value) else: extra_data = one_to_one_extras.get(name, {}) # This is a nested field we need to take care of. value[field.field.name] = obj.id new_value = cls.create_or_update_one_to_one_relation( obj, field, value, extra_data, info) elif isinstance(field, models.ForeignKey): # Delete auto context field here, if it exists. We have to do this explicitly # as we change the name below if name in auto_context_fields: setattr(obj, name, None) name = getattr(field, "db_column", None) or name + "_id" new_value = disambiguate_id(value) elif field_is_many_to_many: new_value = disambiguate_ids(value) if field_is_many_to_many: many_to_many_to_set[name] = new_value else: setattr(obj, name, new_value) # Handle extras fields for name, extras in foreign_key_extras.items(): value = input.get(name, None) field = Model._meta.get_field(name) obj_id = cls.get_or_create_foreign_obj(field, value, extras, info) setattr(obj, name + "_id", obj_id) for name, extras in many_to_many_extras.items(): field = Model._meta.get_field(name) if not name in many_to_many_to_add: many_to_many_to_add[name] = [] many_to_many_to_remove[name] = [] many_to_many_to_set[ name] = None # None means that we should not (re)set the relation. for extra_name, data in extras.items(): field_name = name if extra_name != "exact": field_name = name + "_" + extra_name values = input.get(field_name, None) if isinstance(data, bool): data = {} operation = data.get( "operation") or get_likely_operation_from_name(extra_name) objs = cls.get_or_create_m2m_objs(field, values, data, operation, info) if operation == "exact": many_to_many_to_set[name] = objs elif operation == "add": many_to_many_to_add[name] += objs else: many_to_many_to_remove[name] += objs for name, extras in many_to_one_extras.items(): field = Model._meta.get_field(name) if not name in many_to_one_to_add: many_to_one_to_add[name] = [] many_to_one_to_remove[name] = [] many_to_one_to_set[ name] = None # None means that we should not (re)set the relation. for extra_name, data in extras.items(): field_name = name if extra_name != "exact": field_name = name + "_" + extra_name values = input.get(field_name, None) if values is None: continue if isinstance(data, bool): data = {} operation = data.get( "operation") or get_likely_operation_from_name(extra_name) if operation == "exact": objs = cls.get_or_upsert_m2o_objs(obj, field, values, data, operation, info, Model) many_to_one_to_set[name] = objs elif operation == "add" or operation == "update": objs = cls.get_or_upsert_m2o_objs(obj, field, values, data, operation, info, Model) many_to_one_to_add[name] += objs else: many_to_one_to_remove[name] += disambiguate_ids(values) for name, objs in many_to_one_to_set.items(): if objs is not None: field = getattr(obj, name) if hasattr(field, "remove"): # In this case, the relationship is nullable, and we can clear it, and then add the relevant objects field.clear() field.add(*objs) else: # Remove the related objects by deletion, and set the new ones. field.exclude(id__in=[obj.id for obj in objs]).delete() getattr(obj, name).add(*objs) for name, objs in many_to_one_to_add.items(): getattr(obj, name).add(*objs) for name, objs in many_to_one_to_remove.items(): field = getattr(obj, name) if hasattr(field, "remove"): # The field is nullable, and we simply remove the relation related_name = Model._meta.get_field(name).remote_field.name getattr( obj, name).filter(id__in=objs).update(**{related_name: None}) else: # Only nullable foreign key reverse rels have the remove method. # For other's we have to delete the relations getattr(obj, name).filter(id__in=objs).delete() for name, objs in many_to_many_to_set.items(): if objs is not None: getattr(obj, name).set(objs) for name, objs in many_to_many_to_add.items(): getattr(obj, name).add(*objs) for name, objs in many_to_many_to_remove.items(): getattr(obj, name).remove(*objs) return obj
def test__many_to_one_relation_exists__creates_specified_fields(self): # This registers the UserNode type # noinspection PyUnresolvedReferences from .schema import UserNode class CreateUserMutation(DjangoCreateMutation): class Meta: model = User exclude_fields = ("password", ) class Mutations(graphene.ObjectType): create_user = CreateUserMutation.Field() user = UserFactory.create() cat = CatFactory.create() schema = Schema(mutation=Mutations) mutation = """ mutation CreateUser( $input: CreateUserInput! ){ createUser(input: $input){ user{ id cats{ edges{ node{ id } } } } } } """ result = schema.execute( mutation, variables={ "input": { "username": "******", "email": "*****@*****.**", "firstName": "John", "lastName": "Doe", "cats": [to_global_id("CatNode", cat.id)], }, }, context=Dict(user=user), ) self.assertIsNone(result.errors) data = Dict(result.data) self.assertIsNone(result.errors) self.assertEqual(to_global_id("CatNode", cat.id), data.createUser.user.cats.edges[0].node.id) new_user = User.objects.get( pk=disambiguate_id(data.createUser.user.id)) # Load from database cat.refresh_from_db() self.assertEqual(cat, new_user.cats.first())
def test__reverse_one_to_one_exists__updates_specified_fields(self): # This registers the UserNode type # noinspection PyUnresolvedReferences from .schema import UserNode class CreateDogRegistrationMutation(DjangoCreateMutation): class Meta: model = DogRegistration one_to_one_extras = {"dog": {"type": "auto"}} class Mutations(graphene.ObjectType): create_dog_registration = CreateDogRegistrationMutation.Field() user = UserFactory.create() schema = Schema(mutation=Mutations) mutation = """ mutation CreateDogRegistration( $input: CreateDogRegistrationInput! ){ createDogRegistration(input: $input){ dogRegistration{ id registrationNumber dog{ id name tag breed } } } } """ result = schema.execute( mutation, variables={ "input": { "registrationNumber": "12345", "dog": { "name": "Sparky", "breed": "LABRADOR", "tag": "1234", "owner": user.id, }, }, }, context=Dict(user=user), ) self.assertIsNone(result.errors) data = Dict(result.data) dog_registration = data.createDogRegistration.dogRegistration dog = data.createDogRegistration.dogRegistration.dog self.assertEqual("Sparky", dog.name) self.assertEqual("LABRADOR", dog.breed) self.assertEqual("1234", dog.tag) self.assertEqual("12345", dog_registration.registrationNumber) # Load from database dog_registration = DogRegistration.objects.get( pk=disambiguate_id(dog_registration.id)) dog = getattr(dog_registration, "dog", None) self.assertIsNotNone(dog) self.assertEqual(dog.name, "Sparky") self.assertEqual(dog.tag, "1234")
def validate_name(cls, root, info, value, input, **kwargs): owner = User.objects.get(pk=disambiguate_id(input["owner"])) if value == owner.get_full_name(): raise ValueError("Cat must have different name than owner")
def get_permissions(cls, root, info, id, input, *args, **kwargs): owner_id = int(disambiguate_id(input["owner"])) if info.context.user.id == owner_id: return [] return ["tests.change_cat"]
def mutate(cls, root, info, input): updated_input = cls.before_mutate(root, info, input) if updated_input: input = updated_input if cls._meta.login_required and not info.context.user.is_authenticated: raise GraphQLError("Must be logged in to access this mutation.") cls.check_permissions(root, info, input) Model = cls._meta.model model_field_values = {} for name, value in super(type(input), input).items(): filter_field_split = name.split("__", 1) field_name = filter_field_split[0] try: field = Model._meta.get_field(field_name) except FieldDoesNotExist: # This can happen with nested selectors. In this case we set the field to none. field = None filter_field_is_list = False if len(filter_field_split) > 1: # If we have an "__in" final part of the filter, we are now dealing with # a list of things. Note that all other variants can be coerced directly # on the filter-call, so we don't really have to deal with other cases. filter_field_is_list = filter_field_split[-1] == "in" new_value = value value_handle_name = "handle_" + name if hasattr(cls, value_handle_name): handle_func = getattr(cls, value_handle_name) assert callable( handle_func ), f"Property {value_handle_name} on {cls.__name__} is not a function." new_value = handle_func(value, name, info) # On some fields we perform some default conversion, if the value was not transformed above. if new_value == value and value is not None: if type(field) in (models.ForeignKey, models.OneToOneField): name = getattr(field, "db_column", None) or name + "_id" new_value = disambiguate_id(value) elif ( type(field) in ( models.ManyToManyField, models.ManyToManyRel, models.ManyToOneRel, ) or filter_field_is_list ): new_value = disambiguate_ids(value) model_field_values[name] = new_value filter_qs = cls.get_queryset(root, info, input).filter(**model_field_values) updated_qs = cls.before_save(root, info, filter_qs) if updated_qs: filter_qs = updated_qs ids = [ to_global_id(get_global_registry().get_type_for_model(Model).__name__, id) for id in filter_qs.values_list("id", flat=True) ] deletion_count, _ = filter_qs.delete() cls.after_mutate(root, info, deletion_count, ids) return cls(deletion_count=deletion_count, deleted_ids=ids)
def get_object(cls, root, info, input, full_input): return cls.get_queryset(root, info, full_input).get( pk=disambiguate_id(input["id"]) )
def resolve_id(cls, id): return disambiguate_id(id)