def test_resolve_reverse_related_lookups(self): """ Check that lookups can be resolved for related fields in the reverse direction. """ lookups = [ 'exact', 'gte', 'gt', 'lte', 'lt', 'in', 'isnull', ] # ManyToOneRel model_field = User._meta.get_field('article') for term in lookups: field, lookup = resolve_field(model_field, term) self.assertIsInstance(field, models.ManyToOneRel) self.assertEqual(lookup, term) # ManyToManyRel model_field = Book._meta.get_field('lovers') for term in lookups: field, lookup = resolve_field(model_field, term) self.assertIsInstance(field, models.ManyToManyRel) self.assertEqual(lookup, term)
def test_resolve_forward_related_lookups(self): """ Check that lookups can be resolved for related fields in the forwards direction. """ lookups = [ 'exact', 'gte', 'gt', 'lte', 'lt', 'in', 'isnull', ] # ForeignKey model_field = Article._meta.get_field('author') for term in lookups: field, lookup = resolve_field(model_field, term) self.assertIsInstance(field, models.ForeignKey) self.assertEqual(lookup, term) # ManyToManyField model_field = User._meta.get_field('favorite_books') for term in lookups: field, lookup = resolve_field(model_field, term) self.assertIsInstance(field, models.ManyToManyField) self.assertEqual(lookup, term)
def test_invalid_transformed_lookup_expression(self): model_field = Article._meta.get_field('published') with self.assertRaises(FieldLookupError) as context: resolve_field(model_field, 'date__invalid_lookup') exc = str(context.exception) self.assertIn(str(model_field), exc) self.assertIn('date__invalid_lookup', exc)
def _generate_lookup_expression_filters(cls, filter_name, filter_field): """ For specific filter types, new filters are created based on defined lookup expressions in the form `<field_name>__<lookup_expr>` """ magic_filters = {} if filter_field.method is not None or filter_field.lookup_expr not in ["exact", "in"]: return magic_filters # Choose the lookup expression map based on the filter type lookup_map = cls._get_filter_lookup_dict(filter_field) if lookup_map is None: # Do not augment this filter type with more lookup expressions return magic_filters # Get properties of the existing filter for later use field_name = filter_field.field_name field = get_model_field(cls._meta.model, field_name) # If there isn't a model field, return. if field is None: return magic_filters # Create new filters for each lookup expression in the map for lookup_name, lookup_expr in lookup_map.items(): new_filter_name = "{}__{}".format(filter_name, lookup_name) try: if filter_name in cls.declared_filters: # The filter field has been explicity defined on the filterset class so we must manually # create the new filter with the same type because there is no guarantee the defined type # is the same as the default type for the field resolve_field(field, lookup_expr) # Will raise FieldLookupError if the lookup is invalid new_filter = type(filter_field)( field_name=field_name, lookup_expr=lookup_expr, label=filter_field.label, exclude=filter_field.exclude, distinct=filter_field.distinct, **filter_field.extra, ) else: # The filter field is listed in Meta.fields so we can safely rely on default behaviour # Will raise FieldLookupError if the lookup is invalid new_filter = cls.filter_for_field(field, field_name, lookup_expr) except django_filters.exceptions.FieldLookupError: # The filter could not be created because the lookup expression is not supported on the field continue if lookup_name.startswith("n"): # This is a negation filter which requires a queryset.exclude() clause # Of course setting the negation of the existing filter's exclude attribute handles both cases new_filter.exclude = not filter_field.exclude magic_filters[new_filter_name] = new_filter return magic_filters
def get_additional_lookups(cls, existing_filter_name, existing_filter): new_filters = {} # Skip nonstandard lookup expressions if existing_filter.method is not None or existing_filter.lookup_expr not in ['exact', 'in']: return {} # Choose the lookup expression map based on the filter type lookup_map = cls._get_filter_lookup_dict(existing_filter) if lookup_map is None: # Do not augment this filter type with more lookup expressions return {} # Get properties of the existing filter for later use field_name = existing_filter.field_name field = get_model_field(cls._meta.model, field_name) # Create new filters for each lookup expression in the map for lookup_name, lookup_expr in lookup_map.items(): new_filter_name = f'{existing_filter_name}__{lookup_name}' try: if existing_filter_name in cls.declared_filters: # The filter field has been explicitly defined on the filterset class so we must manually # create the new filter with the same type because there is no guarantee the defined type # is the same as the default type for the field resolve_field(field, lookup_expr) # Will raise FieldLookupError if the lookup is invalid new_filter = type(existing_filter)( field_name=field_name, lookup_expr=lookup_expr, label=existing_filter.label, exclude=existing_filter.exclude, distinct=existing_filter.distinct, **existing_filter.extra ) elif hasattr(existing_filter, 'custom_field'): # Filter is for a custom field custom_field = existing_filter.custom_field new_filter = custom_field.to_filter(lookup_expr=lookup_expr) else: # The filter field is listed in Meta.fields so we can safely rely on default behaviour # Will raise FieldLookupError if the lookup is invalid new_filter = cls.filter_for_field(field, field_name, lookup_expr) except FieldLookupError: # The filter could not be created because the lookup expression is not supported on the field continue if lookup_name.startswith('n'): # This is a negation filter which requires a queryset.exclude() clause # Of course setting the negation of the existing filter's exclude attribute handles both cases new_filter.exclude = not existing_filter.exclude new_filters[new_filter_name] = new_filter return new_filters
def test_resolve_implicit_exact_lookup(self): # Use a DateTimeField, so we can check multiple transforms. # eg, date__year__gte model_field = Article._meta.get_field('published') field, lookup = resolve_field(model_field, 'date') self.assertIsInstance(field, models.DateField) self.assertEqual(lookup, 'exact') field, lookup = resolve_field(model_field, 'date__year') self.assertIsInstance(field, models.IntegerField) self.assertEqual(lookup, 'exact')
def test_resolve_transformed_lookups(self): """ Check that chained field transforms are correctly resolved. eg, a 'date__year__gte' lookup on an article's 'published' timestamp. """ # Use a DateTimeField, so we can check multiple transforms. # eg, date__year__gte model_field = Article._meta.get_field('published') standard_lookups = [ 'exact', 'iexact', 'gte', 'gt', 'lte', 'lt', ] date_lookups = [ 'year', 'month', 'day', 'week_day', ] datetime_lookups = date_lookups + [ 'hour', 'minute', 'second', ] # ex: 'date__gt' for lookup in standard_lookups: field, resolved_lookup = resolve_field( model_field, LOOKUP_SEP.join(['date', lookup])) self.assertIsInstance(field, models.DateField) self.assertEqual(resolved_lookup, lookup) # ex: 'year__iexact' for part in datetime_lookups: for lookup in standard_lookups: field, resolved_lookup = resolve_field( model_field, LOOKUP_SEP.join([part, lookup])) self.assertIsInstance(field, models.IntegerField) self.assertEqual(resolved_lookup, lookup) # ex: 'date__year__lte' for part in date_lookups: for lookup in standard_lookups: field, resolved_lookup = resolve_field( model_field, LOOKUP_SEP.join(['date', part, lookup])) self.assertIsInstance(field, models.IntegerField) self.assertEqual(resolved_lookup, lookup)
def test_resolve_transformed_lookups(self): """ Check that chained field transforms are correctly resolved. eg, a 'date__year__gte' lookup on an article's 'published' timestamp. """ # Use a DateTimeField, so we can check multiple transforms. # eg, date__year__gte model_field = Article._meta.get_field('published') standard_lookups = [ 'exact', 'iexact', 'gte', 'gt', 'lte', 'lt', ] date_lookups = [ 'year', 'month', 'day', 'week_day', ] datetime_lookups = date_lookups + [ 'hour', 'minute', 'second', ] # ex: 'date__gt' for lookup in standard_lookups: field, resolved_lookup = resolve_field(model_field, LOOKUP_SEP.join(['date', lookup])) self.assertIsInstance(field, models.DateField) self.assertEqual(resolved_lookup, lookup) # ex: 'year__iexact' for part in datetime_lookups: for lookup in standard_lookups: field, resolved_lookup = resolve_field(model_field, LOOKUP_SEP.join([part, lookup])) self.assertIsInstance(field, models.IntegerField) self.assertEqual(resolved_lookup, lookup) # ex: 'date__year__lte' for part in date_lookups: for lookup in standard_lookups: field, resolved_lookup = resolve_field(model_field, LOOKUP_SEP.join(['date', part, lookup])) self.assertIsInstance(field, models.IntegerField) self.assertEqual(resolved_lookup, lookup)
def test_resolve_forward_related_lookups(self): """ Check that lookups can be resolved for related fields in the forwards direction. """ lookups = ['exact', 'gte', 'gt', 'lte', 'lt', 'in', 'isnull', ] # ForeignKey model_field = Article._meta.get_field('author') for term in lookups: field, lookup = resolve_field(model_field, term) self.assertIsInstance(field, models.ForeignKey) self.assertEqual(lookup, term) # ManyToManyField model_field = User._meta.get_field('favorite_books') for term in lookups: field, lookup = resolve_field(model_field, term) self.assertIsInstance(field, models.ManyToManyField) self.assertEqual(lookup, term)
def test_resolve_reverse_related_lookups(self): """ Check that lookups can be resolved for related fields in the reverse direction. """ lookups = ['exact', 'gte', 'gt', 'lte', 'lt', 'in', 'isnull', ] # ManyToOneRel model_field = User._meta.get_field('article') for term in lookups: field, lookup = resolve_field(model_field, term) self.assertIsInstance(field, models.ManyToOneRel) self.assertEqual(lookup, term) # ManyToManyRel model_field = Book._meta.get_field('lovers') for term in lookups: field, lookup = resolve_field(model_field, term) self.assertIsInstance(field, models.ManyToManyRel) self.assertEqual(lookup, term)
def test_resolve_plain_lookups(self): """ Check that the standard query terms can be correctly resolved. eg, an 'EXACT' lookup on a user's username """ model_field = User._meta.get_field('username') lookups = model_field.class_lookups.keys() # This is simple - the final ouput of an untransformed field is itself. # The lookups are the default lookups registered to the class. for term in lookups: field, lookup = resolve_field(model_field, term) self.assertIsInstance(field, models.CharField) self.assertEqual(lookup, term)
def filter_for_field(cls, f, name, lookup_expr='exact'): # Redefine método estático para ignorar filtro para # fields que não possuam lookup_expr informado f, lookup_type = resolve_field(f, lookup_expr) default = { 'field_name': name, 'label': capfirst(f.verbose_name), 'lookup_expr': lookup_expr } filter_class, params = cls.filter_for_lookup(f, lookup_type) default.update(params) if filter_class is not None: return filter_class(**default) return None
def test_invalid_transformed_lookup_expression(self): model_field = Article._meta.get_field('published') with self.assertRaises(FieldError): field, lookup = resolve_field(model_field, 'date__invalid_lookup')
def get_filters(cls): """ Override filter generation to support dynamic lookup expressions for certain filter types. For specific filter types, new filters are created based on defined lookup expressions in the form `<field_name>__<lookup_expr>` """ filters = super().get_filters() new_filters = {} for existing_filter_name, existing_filter in filters.items(): # Loop over existing filters to extract metadata by which to create new filters # If the filter makes use of a custom filter method or lookup expression skip it # as we cannot sanely handle these cases in a generic mannor if existing_filter.method is not None or existing_filter.lookup_expr not in ["exact", "in"]: continue # Choose the lookup expression map based on the filter type lookup_map = cls._get_filter_lookup_dict(existing_filter) if lookup_map is None: # Do not augment this filter type with more lookup expressions continue # Get properties of the existing filter for later use field_name = existing_filter.field_name field = get_model_field(cls._meta.model, field_name) # Create new filters for each lookup expression in the map for lookup_name, lookup_expr in lookup_map.items(): new_filter_name = "{}__{}".format(existing_filter_name, lookup_name) try: if existing_filter_name in cls.declared_filters: # The filter field has been explicity defined on the filterset class so we must manually # create the new filter with the same type because there is no guarantee the defined type # is the same as the default type for the field resolve_field(field, lookup_expr) # Will raise FieldLookupError if the lookup is invalid new_filter = type(existing_filter)( field_name=field_name, lookup_expr=lookup_expr, label=existing_filter.label, exclude=existing_filter.exclude, distinct=existing_filter.distinct, **existing_filter.extra, ) else: # The filter field is listed in Meta.fields so we can safely rely on default behaviour # Will raise FieldLookupError if the lookup is invalid new_filter = cls.filter_for_field(field, field_name, lookup_expr) except django_filters.exceptions.FieldLookupError: # The filter could not be created because the lookup expression is not supported on the field continue if lookup_name.startswith("n"): # This is a negation filter which requires a queryset.exclude() clause # Of course setting the negation of the existing filter's exclude attribute handles both cases new_filter.exclude = not existing_filter.exclude new_filters[new_filter_name] = new_filter filters.update(new_filters) return filters