Beispiel #1
0
    def test__ids_does_not_exist__only_deletes_relevant_objects(self):
        # This registers the UserNode type
        # noinspection PyUnresolvedReferences
        from .schema import UserNode

        class BatchDeleteUserMutation(DjangoBatchDeleteMutation):
            class Meta:
                model = User

        class Mutations(graphene.ObjectType):
            batch_delete_user = BatchDeleteUserMutation.Field()

        users = UserFactory.create_batch(10)

        selection = random.sample(users, k=5)
        non_existing_ids = list(range(-1, -5))

        ids_to_delete = [user.id for user in selection] + non_existing_ids

        schema = Schema(mutation=Mutations)
        mutation = """
            mutation BatchDeleteUser(
                $ids: [ID]!,
            ){
                batchDeleteUser(ids: $ids){
                    deletedIds
                    missedIds
                    deletionCount
                }
            }
        """

        result = schema.execute(
            mutation,
            variables={"ids": ids_to_delete},
        )
        data = Dict(result.data)
        self.assertEqual(5, data.batchDeleteUser.deletionCount)
        self.assertListEqual(
            list(sorted([str(user.id) for user in selection])),
            list(sorted(disambiguate_ids(data.batchDeleteUser.deletedIds))),
        )
        self.assertListEqual(
            non_existing_ids,
            list(sorted(disambiguate_ids(data.batchDeleteUser.missedIds))),
        )
Beispiel #2
0
    def mutate(cls, root, info, ids):
        cls.before_mutate(root, info, ids)

        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, ids)

        Model = cls._meta.model
        ids = disambiguate_ids(ids)

        cls.validate(root, info, ids)

        qs_to_delete = cls.get_queryset(root, info, ids).filter(id__in=ids)

        updated_qs = cls.before_save(root, info, qs_to_delete)

        if updated_qs:
            qs_to_delete = updated_qs

        # Find out which (global) ids are deleted, and which were not found.
        deleted_ids = [
            to_global_id(
                get_global_registry().get_type_for_model(Model).__name__, id)
            for id in qs_to_delete.values_list("id", flat=True)
        ]

        all_global_ids = [
            to_global_id(
                get_global_registry().get_type_for_model(Model).__name__, id)
            for id in ids
        ]

        missed_ids = list(set(all_global_ids).difference(deleted_ids))

        deletion_count, _ = qs_to_delete.delete()

        cls.after_mutate(root, info, deletion_count, deleted_ids)

        return cls(deletion_count=deletion_count,
                   deleted_ids=deleted_ids,
                   missed_ids=missed_ids)
    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)
Beispiel #4
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
Beispiel #5
0
 def resolve_ids(cls, ids):
     return disambiguate_ids(ids)