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))), )
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)
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 resolve_ids(cls, ids): return disambiguate_ids(ids)