Example #1
0
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
Example #2
0
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.'
    )
Example #3
0
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)
Example #4
0
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)