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)]
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]
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
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
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
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
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
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