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())
Example #2
0
    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)
Example #3
0
    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")
Example #5
0
    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
Example #8
0
    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)))
Example #10
0
    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,
            )
Example #11
0
    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"])
     )
Example #18
0
 def resolve_id(cls, id):
     return disambiguate_id(id)