def test_mappings(model_name, config): """ Test that `MAPPINGS` includes all the data necessary for covering all the cases. This is to avoid missing tests when new fields and models are added or changed. """ model = apps.get_model(model_name) try: mapping = MAPPINGS[model_name] except KeyError: pytest.fail(f'Please add test cases for deleting orphaned {model}') related_fields = get_related_fields(model) expected_related_deps = {(field.field.model, field.field.name) for field in related_fields if field not in config.excluded_relations} related_deps_in_mapping = { (dep_factory._meta.model, dep_field_name) for dep_factory, dep_field_name in mapping['dependent_models'] } ignored_relations = { (apps.get_model(model_label), field_name) for model_label, field_name in mapping['ignored_models'] } missing_dep_mappings = expected_related_deps - related_deps_in_mapping - ignored_relations if missing_dep_mappings: dep_list = [ f'{model}.{field}' for model, field in missing_dep_mappings ] error_msg = ( f'Please add tests for not deleting {model} when the following ' f'fields reference it: {", ".join(dep_list)}') assert not missing_dep_mappings, error_msg
def test_configs(model_label, config): """ Test that configs for delete_old_records cover all relations for the model. This is to make sure any new relations that are added are not missed from the configurations. """ model = apps.get_model(model_label) related_fields = get_related_fields(model) field_missing_from_config = ( set(related_fields) - (config.relation_filter_mapping or {}).keys() - set(config.excluded_relations)) fields_for_error_message = '\n'.join( (_format_field(field) for field in field_missing_from_config), ) assert not field_missing_from_config, ( f'The following related fields are missing from the config for {model_label}:\n' f'{fields_for_error_message}.\n' f'\n' f' Please add them to the ModelCleanupConfig in either ' f'relation_filter_mapping or excluded_relations.\n' f'\n' f'Only add the model to excluded_relations if its existence should not affect ' f'whether {model_label} objects are be deleted. You can specify an empty filter ' f'list in relation_filter_mapping if they should not be filtered.\n' f'\n' f'See ModelCleanupConfig and the delete_old_records command for more details.' )
def get_unreferenced_objects_query( model, excluded_relations=(), relation_exclusion_filter_mapping=None, ): """ Generates a query set of unreferenced objects for a model. :param model: the model to generate a query set of unreferenced objects :param excluded_relations: related fields on model that should be ignored :param relation_exclusion_filter_mapping: Optional mapping of relations (fields on model) to Q objects. For each relation where a Q object is provided, the Q object is used to exclude objects for that relation prior to checking if any references to the model exist (for that relation). Example: This example will not consider interactions dated before 2015-01-01 when getting unreferenced companies. get_unreferenced_objects_query( Company, relation_exclusion_filter_mapping={ Company._meta.get_field('interactions'): Q(date__lt=date(2015, 1, 1), } ) :returns: queryset for unreferenced objects """ if relation_exclusion_filter_mapping is None: relation_exclusion_filter_mapping = {} fields = set(get_related_fields(model)) - set(excluded_relations) identifiers = [f'ann_{token_urlsafe(6)}' for _ in range(len(fields))] if relation_exclusion_filter_mapping.keys() - fields: raise ValueError( 'Invalid fields detected in relation_exclusion_filter_mapping.') qs = model.objects.all() for identifier, field in zip(identifiers, fields): related_field = field.field exclusion_filters = relation_exclusion_filter_mapping.get(field, Q()) subquery = related_field.model.objects.filter( **{ related_field.attname: OuterRef('pk') }, ).exclude(exclusion_filters, ).only('pk') qs = qs.annotate(**{identifier: Exists(subquery)}) filter_args = {identifier: False for identifier in identifiers} return qs.filter(**filter_args)
def is_company_a_valid_merge_source(company: Company): """Checks if company can be moved.""" # First, check that there are no references to the company from other objects # (other than via the fields specified in ALLOWED_RELATIONS_FOR_MERGING). relations = get_related_fields(Company) has_related_objects = any( getattr(company, relation.name).count() for relation in relations if relation.remote_field not in ALLOWED_RELATIONS_FOR_MERGING) if has_related_objects: return False # Then, check that the source company itself doesn't have any references to other # companies. self_referential_fields = get_self_referential_relations(Company) return not any( getattr(company, field.name) for field in self_referential_fields)