class TestQueryableProperty(object):
    @pytest.mark.parametrize('kwargs', [
        {},
        {
            'verbose_name': 'Test Property'
        },
    ])
    def test_initializer(self, kwargs):
        prop = QueryableProperty(**kwargs)
        assert prop.name is None
        assert prop.model is None
        assert prop.setter_cache_behavior is CLEAR_CACHE
        assert prop.verbose_name == kwargs.get('verbose_name')

    def test_short_description(self):
        prop = QueryableProperty(verbose_name='Test Property')
        assert prop.short_description == 'Test Property'

    def test_contribute_to_class(self, dummy_property, model_instance):
        descriptor = getattr(model_instance.__class__, dummy_property.name)
        assert isinstance(descriptor, QueryablePropertyDescriptor)
        assert descriptor.prop is dummy_property
        assert isinstance(dummy_property, QueryableProperty)
        assert dummy_property.name == 'dummy'
        assert dummy_property.verbose_name == 'Dummy'
        assert dummy_property.model is model_instance.__class__
        assert six.get_method_function(
            model_instance.reset_property) is reset_queryable_property
        # TODO: test that an existing method with the name reset_property will not be overridden

    @pytest.mark.parametrize(
        'model',
        [VersionWithClassBasedProperties, VersionWithDecoratorBasedProperties])
    def test_pickle_unpickle(self, model):
        prop = get_queryable_property(model, 'version')
        serialized_prop = six.moves.cPickle.dumps(prop)
        deserialized_prop = six.moves.cPickle.loads(serialized_prop)
        assert deserialized_prop is prop

    @pytest.mark.parametrize('prop, expected_str, expected_class_name', [
        (get_queryable_property(ApplicationWithClassBasedProperties, 'dummy'),
         'tests.app_management.models.ApplicationWithClassBasedProperties.dummy',
         'DummyProperty'),
        (get_queryable_property(VersionWithClassBasedProperties, 'is_beta'),
         'tests.dummy_lib.models.ReleaseTypeModel.is_beta',
         'ValueCheckProperty'),
    ])
    def test_representations(self, prop, expected_str, expected_class_name):
        assert six.text_type(prop) == expected_str
        assert repr(prop) == '<{}: {}>'.format(expected_class_name,
                                               expected_str)

    def test_invalid_property_name(self):
        with pytest.raises(QueryablePropertyError,
                           match='must not contain the lookup separator'):
            type(
                'BrokenModel', (Model, ), {
                    'dummy__dummy': DummyProperty(),
                    '__module__': 'tests.app_management.models'
                })
 def test_get_filter(self, lookups, relation_path, expected_filter):
     prop = get_queryable_property(ApplicationWithClassBasedProperties,
                                   'version_count')
     ref = QueryablePropertyReference(prop, prop.model, relation_path)
     q = ref.get_filter(lookups, 1337)
     assert isinstance(q, Q)
     assert q.children == [(expected_filter, 1337)]
Esempio n. 3
0
 def test_filter_based_on_non_relation_field(self, monkeypatch, categories,
                                             applications, negated):
     monkeypatch.setattr(
         get_queryable_property(ApplicationWithClassBasedProperties,
                                'has_version_with_changelog'), 'negated',
         negated)
     app_queryset = ApplicationWithClassBasedProperties.objects.all()
     category_queryset = CategoryWithClassBasedProperties.objects.all()
     assert set(app_queryset.filter(
         has_version_with_changelog=not negated)) == set(applications[:2])
     assert not app_queryset.filter(
         has_version_with_changelog=negated).exists()
     assert (set(
         category_queryset.filter(
             applications__has_version_with_changelog=not negated)) == set(
                 categories[:2]))
     assert not category_queryset.filter(
         applications__has_version_with_changelog=negated).exists()
     applications[1].versions.filter(major=2).delete()
     assert app_queryset.get(
         has_version_with_changelog=not negated) == applications[0]
     assert app_queryset.get(
         has_version_with_changelog=negated) == applications[1]
     assert category_queryset.get(
         applications__has_version_with_changelog=not negated
     ) == categories[0]
     assert category_queryset.get(
         applications__has_version_with_changelog=negated) == categories[1]
Esempio n. 4
0
def refs():
    model = ApplicationWithClassBasedProperties
    return {
        prop_name:
        QueryablePropertyReference(get_queryable_property(model, prop_name),
                                   model, QueryPath())
        for prop_name in ('major_sum', 'version_count')
    }
 def test_filter_based_on_transform(self, monkeypatch, include_boundaries, include_missing, in_range, condition,
                                    expected_versions):
     prop = get_queryable_property(VersionWithClassBasedProperties, 'supported_in_2018')
     monkeypatch.setattr(prop, 'include_boundaries', include_boundaries)
     monkeypatch.setattr(prop, 'include_missing', include_missing)
     monkeypatch.setattr(prop, 'in_range', in_range)
     results = VersionWithClassBasedProperties.objects.filter(condition)
     assert set(version.version for version in results) == expected_versions
Esempio n. 6
0
 def test_build_subquery(self, monkeypatch, categories, negated, delete_v2,
                         expected_result):
     if delete_v2:
         VersionWithClassBasedProperties.objects.filter(major=2).delete()
     prop = get_queryable_property(CategoryWithClassBasedProperties,
                                   'has_v2')
     monkeypatch.setattr(prop, 'negated', negated)
     assert categories[0].has_v2 is expected_result
Esempio n. 7
0
 def test_getter(self, monkeypatch, categories, applications, negated):
     monkeypatch.setattr(
         get_queryable_property(CategoryWithClassBasedProperties,
                                'has_versions'), 'negated', negated)
     assert categories[0].has_versions is not negated
     assert categories[1].has_versions is not negated
     applications[1].versions.all().delete()
     assert categories[0].has_versions is not negated
     assert categories[1].has_versions is negated
 def test_getter(self, monkeypatch, versions, index, prop_name, value, include_boundaries, include_missing,
                 in_range, expected_result):
     version = versions[index]
     prop = get_queryable_property(VersionWithClassBasedProperties, prop_name)
     monkeypatch.setattr(prop, 'value', value)
     monkeypatch.setattr(prop, 'include_boundaries', include_boundaries)
     monkeypatch.setattr(prop, 'include_missing', include_missing)
     monkeypatch.setattr(prop, 'in_range', in_range)
     assert getattr(version, prop_name) is expected_result
Esempio n. 9
0
 def test_filter(self, monkeypatch, categories, applications, negated):
     monkeypatch.setattr(
         get_queryable_property(CategoryWithClassBasedProperties,
                                'has_versions'), 'negated', negated)
     queryset = CategoryWithClassBasedProperties.objects.all()
     assert set(queryset.filter(has_versions=not negated)) == set(
         categories[:2])
     assert not queryset.filter(has_versions=negated).exists()
     applications[1].versions.all().delete()
     assert queryset.get(has_versions=not negated) == categories[0]
     assert queryset.get(has_versions=negated) == categories[1]
 def test_filter_implementation_used_despite_present_annotation(
         self, monkeypatch, model):
     # Patch the property to have a filter that is always True, then use a
     # condition that would be False without the patch.
     prop = get_queryable_property(model, 'version_count')
     monkeypatch.setattr(prop, 'get_filter',
                         lambda cls, lookup, value: models.Q(pk__gt=0))
     queryset = model.objects.select_properties('version_count').filter(
         version_count__gt=5)
     assert '"id" > 0' in six.text_type(queryset.query)
     assert queryset.count() == len(queryset) == 2
Esempio n. 11
0
 def test_getter_based_on_non_relation_field(self, monkeypatch,
                                             applications, negated):
     monkeypatch.setattr(
         get_queryable_property(ApplicationWithClassBasedProperties,
                                'has_version_with_changelog'), 'negated',
         negated)
     assert applications[0].has_version_with_changelog is not negated
     assert applications[1].has_version_with_changelog is not negated
     applications[0].versions.filter(major=2).delete()
     assert applications[0].has_version_with_changelog is negated
     assert applications[1].has_version_with_changelog is not negated
 def test_get_annotation_exception(self):
     prop = get_queryable_property(ApplicationWithClassBasedProperties,
                                   'dummy')
     ref = QueryablePropertyReference(prop, prop.model, QueryPath())
     with pytest.raises(QueryablePropertyError):
         ref.get_annotation()
class TestResolveQueryableProperty(object):
    @pytest.mark.parametrize(
        'model, query_path, expected_property, expected_lookups',
        [
            # No relation involved
            (VersionWithClassBasedProperties, QueryPath('version'),
             get_queryable_property(VersionWithClassBasedProperties,
                                    'version'), QueryPath()),
            (VersionWithDecoratorBasedProperties, QueryPath('version'),
             get_queryable_property(VersionWithDecoratorBasedProperties,
                                    'version'), QueryPath()),
            (VersionWithClassBasedProperties,
             QueryPath('version__lower__exact'),
             get_queryable_property(VersionWithClassBasedProperties,
                                    'version'), QueryPath('lower__exact')),
            (VersionWithDecoratorBasedProperties,
             QueryPath('version__lower__exact'),
             get_queryable_property(VersionWithDecoratorBasedProperties,
                                    'version'), QueryPath('lower__exact')),
            # FK forward relation
            (VersionWithClassBasedProperties,
             QueryPath('application__version_count'),
             get_queryable_property(ApplicationWithClassBasedProperties,
                                    'version_count'), QueryPath()),
            (VersionWithDecoratorBasedProperties,
             QueryPath('application__version_count'),
             get_queryable_property(ApplicationWithDecoratorBasedProperties,
                                    'version_count'), QueryPath()),
            (VersionWithClassBasedProperties,
             QueryPath('application__major_sum__gt'),
             get_queryable_property(ApplicationWithClassBasedProperties,
                                    'major_sum'), QueryPath('gt')),
            (VersionWithDecoratorBasedProperties,
             QueryPath('application__major_sum__gt'),
             get_queryable_property(ApplicationWithDecoratorBasedProperties,
                                    'major_sum'), QueryPath('gt')),
            # FK reverse relation
            (ApplicationWithClassBasedProperties,
             QueryPath('versions__major_minor'),
             get_queryable_property(VersionWithClassBasedProperties,
                                    'major_minor'), QueryPath()),
            (ApplicationWithDecoratorBasedProperties,
             QueryPath('versions__major_minor'),
             get_queryable_property(VersionWithDecoratorBasedProperties,
                                    'major_minor'), QueryPath()),
            (ApplicationWithClassBasedProperties,
             QueryPath('versions__version__lower__contains'),
             get_queryable_property(VersionWithClassBasedProperties,
                                    'version'), QueryPath('lower__contains')),
            (ApplicationWithDecoratorBasedProperties,
             QueryPath('versions__version__lower__contains'),
             get_queryable_property(VersionWithDecoratorBasedProperties,
                                    'version'), QueryPath('lower__contains')),
            # M2M forward relation
            (ApplicationWithClassBasedProperties,
             QueryPath('categories__circular'),
             get_queryable_property(CategoryWithClassBasedProperties,
                                    'circular'), QueryPath()),
            (ApplicationWithDecoratorBasedProperties,
             QueryPath('categories__circular'),
             get_queryable_property(CategoryWithDecoratorBasedProperties,
                                    'circular'), QueryPath()),
            (ApplicationWithClassBasedProperties,
             QueryPath('categories__circular__exact'),
             get_queryable_property(CategoryWithClassBasedProperties,
                                    'circular'), QueryPath('exact')),
            (ApplicationWithDecoratorBasedProperties,
             QueryPath('categories__circular__exact'),
             get_queryable_property(CategoryWithDecoratorBasedProperties,
                                    'circular'), QueryPath('exact')),
            # M2M reverse relation
            (CategoryWithClassBasedProperties,
             QueryPath('applications__major_sum'),
             get_queryable_property(ApplicationWithClassBasedProperties,
                                    'major_sum'), QueryPath()),
            (CategoryWithDecoratorBasedProperties,
             QueryPath('applications__major_sum'),
             get_queryable_property(ApplicationWithDecoratorBasedProperties,
                                    'major_sum'), QueryPath()),
            (CategoryWithClassBasedProperties,
             QueryPath('applications__version_count__lt'),
             get_queryable_property(ApplicationWithClassBasedProperties,
                                    'version_count'), QueryPath('lt')),
            (CategoryWithDecoratorBasedProperties,
             QueryPath('applications__version_count__lt'),
             get_queryable_property(ApplicationWithDecoratorBasedProperties,
                                    'version_count'), QueryPath('lt')),
            # Multiple relations
            (CategoryWithClassBasedProperties,
             QueryPath(
                 'applications__versions__application__categories__circular'),
             get_queryable_property(CategoryWithClassBasedProperties,
                                    'circular'), QueryPath()),
            (CategoryWithDecoratorBasedProperties,
             QueryPath(
                 'applications__versions__application__categories__circular'),
             get_queryable_property(CategoryWithDecoratorBasedProperties,
                                    'circular'), QueryPath()),
            (VersionWithClassBasedProperties,
             QueryPath('application__categories__circular__in'),
             get_queryable_property(CategoryWithClassBasedProperties,
                                    'circular'), QueryPath('in')),
            (VersionWithDecoratorBasedProperties,
             QueryPath('application__categories__circular__in'),
             get_queryable_property(CategoryWithDecoratorBasedProperties,
                                    'circular'), QueryPath('in')),
        ])
    def test_successful(self, model, query_path, expected_property,
                        expected_lookups):
        expected_ref = QueryablePropertyReference(
            expected_property, expected_property.model,
            query_path[:-len(expected_lookups) - 1])
        assert resolve_queryable_property(model,
                                          query_path) == (expected_ref,
                                                          expected_lookups)

    @pytest.mark.parametrize(
        'model, query_path',
        [
            # No relation involved
            (VersionWithClassBasedProperties, QueryPath('non_existent')),
            (VersionWithDecoratorBasedProperties, QueryPath('non_existent')),
            (VersionWithClassBasedProperties, QueryPath('major')),
            (VersionWithDecoratorBasedProperties, QueryPath('major')),
            # FK forward relation
            (VersionWithClassBasedProperties,
             QueryPath('application__non_existent__exact')),
            (VersionWithDecoratorBasedProperties,
             QueryPath('application__non_existent__exact')),
            (VersionWithClassBasedProperties, QueryPath('application__name')),
            (VersionWithDecoratorBasedProperties,
             QueryPath('application__name')),
            # FK reverse relation
            (ApplicationWithClassBasedProperties,
             QueryPath('versions__non_existent')),
            (ApplicationWithDecoratorBasedProperties,
             QueryPath('versions__non_existent')),
            (ApplicationWithClassBasedProperties,
             QueryPath('versions__minor__gt')),
            (ApplicationWithDecoratorBasedProperties,
             QueryPath('versions__minor__gt')),
            # M2M forward relation
            (ApplicationWithClassBasedProperties,
             QueryPath('categories__non_existent')),
            (ApplicationWithDecoratorBasedProperties,
             QueryPath('categories__non_existent')),
            (ApplicationWithClassBasedProperties,
             QueryPath('categories__name')),
            (ApplicationWithDecoratorBasedProperties,
             QueryPath('categories__name')),
            # M2M reverse relation
            (CategoryWithClassBasedProperties,
             QueryPath('applications__non_existent')),
            (CategoryWithDecoratorBasedProperties,
             QueryPath('applications__non_existent')),
            (CategoryWithClassBasedProperties,
             QueryPath('applications__name')),
            (CategoryWithDecoratorBasedProperties,
             QueryPath('applications__name')),
            # Non existent relation
            (VersionWithClassBasedProperties,
             QueryPath('non_existent_relation__non_existent__in')),
            (VersionWithDecoratorBasedProperties,
             QueryPath('non_existent_relation__non_existent__in')),
        ])
    def test_unsuccessful(self, model, query_path):
        assert resolve_queryable_property(model,
                                          query_path) == (None, QueryPath())
class TestQueryablePropertiesAdminMixin(object):
    @pytest.mark.parametrize('admin_class, model, expected_value', [
        (VersionAdmin, VersionWithClassBasedProperties, ()),
        (ApplicationAdmin, ApplicationWithClassBasedProperties,
         ('version_count', )),
        (VersionInline, ApplicationWithClassBasedProperties,
         ('changes_or_default', )),
    ])
    def test_get_list_select_properties(self, rf, admin_class, model,
                                        expected_value):
        admin = admin_class(model, site)
        assert admin.get_list_select_properties(rf.get('/')) == expected_value

    @pytest.mark.parametrize(
        'admin_class, model, apply_patch, expected_selected_properties', [
            (VersionAdmin, VersionWithClassBasedProperties, False, ()),
            (VersionAdmin, VersionWithClassBasedProperties, True, ()),
            (ApplicationAdmin, ApplicationWithClassBasedProperties, False,
             (get_queryable_property(ApplicationWithClassBasedProperties,
                                     'version_count'), )),
            (ApplicationAdmin, ApplicationWithClassBasedProperties, True,
             (get_queryable_property(ApplicationWithClassBasedProperties,
                                     'version_count'), )),
        ])
    def test_get_queryset(self, rf, admin_class, model, apply_patch,
                          expected_selected_properties):
        admin = admin_class(model, site)
        qs_patch = nullcontext()
        if apply_patch:
            qs_patch = patch(
                'django.contrib.admin.options.ModelAdmin.{}'.format(
                    ADMIN_QUERYSET_METHOD_NAME),
                return_value=QuerySet(model))

        with qs_patch:
            queryset = admin.get_queryset(rf.get('/'))
        assert queryset.model is model
        assert isinstance(queryset, QueryablePropertiesQuerySetMixin)
        assert len(queryset.query._queryable_property_annotations) == len(
            expected_selected_properties)
        for prop in expected_selected_properties:
            assert any(
                ref.property is prop
                for ref in queryset.query._queryable_property_annotations)

    @pytest.mark.parametrize('list_filter_item, property_name', [
        ('name', None),
        (DummyListFilter, None),
        ('support_start_date', 'support_start_date'),
        (('support_start_date', ChoicesFieldListFilter), 'support_start_date'),
    ])
    def test_get_list_filter(self, monkeypatch, rf, list_filter_item,
                             property_name):
        monkeypatch.setattr(ApplicationAdmin, 'list_filter',
                            ('common_data', list_filter_item))
        admin = ApplicationAdmin(ApplicationWithClassBasedProperties, site)
        list_filter = admin.list_filter
        if DJANGO_VERSION >= (1, 5):
            list_filter = admin.get_list_filter(rf.get('/'))
        assert list_filter[0] == 'common_data'
        assert (list_filter[1] == list_filter_item) is (not property_name)
        if property_name:
            replacement = list_filter[1]
            assert callable(replacement)
            filter_instance = replacement(rf.get('/'), {}, admin.model, admin)
            assert isinstance(filter_instance, FieldListFilter)
            assert filter_instance.field.name == property_name

    @pytest.mark.skipif(
        DJANGO_VERSION < (2, 1),
        reason='Arbitrary search fields were not allowed before Django 2.1')
    @pytest.mark.django_db
    @pytest.mark.parametrize('search_term, expected_count', [
        ('app', 2),
        ('cool', 1),
        ('another', 1),
        ('not-found', 0),
        ('2.0.0', 1),
        ('1.3.1', 1),
        ('1.3', 1),
        ('1.3.0', 0),
        ('1.2.3', 0),
        ('3.4.5', 0),
    ])
    @pytest.mark.usefixtures('versions')
    def test_get_search_results(self, rf, applications, search_term,
                                expected_count):
        applications[0].versions.filter(version='2.0.0').delete()
        request = rf.get('/')
        admin = ApplicationAdmin(ApplicationWithClassBasedProperties, site)
        queryset = admin.get_search_results(request,
                                            admin.get_queryset(request),
                                            search_term)[0]
        assert queryset.count() == expected_count
 def test_get_annotation(self):
     prop = get_queryable_property(ApplicationWithClassBasedProperties,
                                   'version_count')
     ref = QueryablePropertyReference(prop, prop.model, QueryPath())
     assert isinstance(ref.get_annotation(), Count)
 def test_full_path(self, relation_path, expected_result):
     prop = get_queryable_property(ApplicationWithClassBasedProperties,
                                   'dummy')
     ref = QueryablePropertyReference(prop, prop.model, relation_path)
     assert ref.full_path == expected_result
 def test_descriptor(self):
     prop = get_queryable_property(ApplicationWithClassBasedProperties,
                                   'dummy')
     ref = QueryablePropertyReference(prop, prop.model, QueryPath())
     assert ref.descriptor == ApplicationWithClassBasedProperties.dummy
 def test_pickle_unpickle(self, model):
     prop = get_queryable_property(model, 'version')
     serialized_prop = six.moves.cPickle.dumps(prop)
     deserialized_prop = six.moves.cPickle.loads(serialized_prop)
     assert deserialized_prop is prop
 def test_final_value(self, monkeypatch):
     prop = get_queryable_property(VersionWithClassBasedProperties, 'is_supported')
     assert prop.final_value == date(2019, 1, 1)
     monkeypatch.setattr(prop, 'value', lambda: 5)
     assert prop.final_value == 5
def dummy_property():
    prop = get_queryable_property(ApplicationWithClassBasedProperties, 'dummy')
    prop.counter = 0
    return prop
 def test_exception(self, model, property_name):
     with pytest.raises(QueryablePropertyDoesNotExist):
         get_queryable_property(model, property_name)
 def nested_prop(self):
     prop = get_queryable_property(ApplicationWithClassBasedProperties,
                                   'major_avg')
     assert isinstance(prop, AnnotationGetterMixin)
     return prop
Esempio n. 23
0
 def test_build_subquery(self, monkeypatch, applications, field_name,
                         expected_value):
     prop = get_queryable_property(ApplicationWithClassBasedProperties,
                                   'highest_version')
     monkeypatch.setattr(prop, 'field_name', field_name)
     assert applications[0].highest_version == expected_value
 def test_exception_on_unimplemented_filter(self, monkeypatch, model):
     prop = get_queryable_property(model, 'version')
     monkeypatch.setattr(prop, 'get_filter', None)
     with pytest.raises(QueryablePropertyError):
         model.objects.filter(version='1.2.3')
 def test_property_found(self, model, property_name):
     prop = get_queryable_property(model, property_name)
     assert isinstance(prop, QueryableProperty)
 def prop(self):
     prop = get_queryable_property(ApplicationWithClassBasedProperties,
                                   'version_count')
     assert isinstance(prop, AnnotationGetterMixin)
     return prop