Skip to content

Commit

Permalink
refactor: move filtermethods from apis_entities to utils
Browse files Browse the repository at this point in the history
The apis_entities.filters.GenericEntityListFilter contained some filter
methods (as defined in
https://django-filter.readthedocs.io/en/main/ref/filters.html#method)
and a helper method that could be useful to other modules. Therefore
those methods are being moved to `utils.filtermethods`
The `name_label_filter` method was removed, because the label module
will be dropped, therefore the filter is not useful anymore.
The function names in the `apis_core.utils.filtermethods` module were
renamed and harmonized, as to not end in `_method` or `_filter`.

Closes: #152
  • Loading branch information
b1rger committed May 4, 2023
1 parent f828511 commit ac9fa86
Show file tree
Hide file tree
Showing 2 changed files with 100 additions and 141 deletions.
141 changes: 0 additions & 141 deletions apis_core/apis_entities/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,147 +147,6 @@ def set_and_sort_filters(self, filters):

return sorted_filters

def construct_lookup(self, value):
"""
Parses user input for wildcards and returns a tuple containing the interpreted django lookup string and the trimmed value
E.g.
'example' -> ('__icontains', 'example')
'*example' -> ('__iendswith', 'example')
'example*' -> ('__istartswith', 'example')
'"example"' -> ('__iexact', 'example')
:param value : str : text to be parsed for *
:return: (lookup : str, value : str)
"""

if value.startswith("*") and not value.endswith("*"):
value = value[1:]
return "__iendswith", value

elif not value.startswith("*") and value.endswith("*"):
value = value[:-1]
return "__istartswith", value

elif value.startswith('"') and value.endswith('"'):
value = value[1:-1]
return "__iexact", value

else:
if value.startswith("*") and value.endswith("*"):
value = value[1:-1]
return "__icontains", value

def name_label_filter(self, queryset, name, value):
# TODO: include alternative names queries
lookup, value = self.construct_lookup(value)

queryset_related_label = queryset.filter(**{"label__label" + lookup: value})
queryset_self_name = queryset.filter(**{name + lookup: value})

return (queryset_related_label | queryset_self_name).distinct().all()

# TODO RDF: Check if this should be removed or adapted
def related_entity_name_method(self, queryset, name, value):
lookup, value = self.construct_lookup(value)

queryset = queryset.filter(
Q(**{f"triple_set_from_obj__subj__name{lookup}": value})
| Q(**{f"triple_set_from_subj__obj__name{lookup}": value})
).distinct()

return queryset

def related_property_name_method(self, queryset, name, value):
# TODO RDF: Check if this should be removed or adapted_
#
# """
# Searches through the all name fields of all related relationtypes of a given queryset
#
# The following logic is almost identical to the one in method 'related_entity_name_method', so please look up its
# comments for documentational purpose therein.
#
# Differences are commented however.
#
# :param queryset: the queryset currently to be filtered on
# :param name: Here not used, because this method filters on names of related relationtypes
# :param value: The value to be filtered for, input by the user or a programmatic call
# :return: filtered queryset
# """
#
# lookup, value = self.construct_lookup(value)
#
# # look up through name and name_reverse of Property
# property_hit = (
# Property.objects.filter(**{"name" + lookup: value}).values_list("pk", flat=True) |
# Property.objects.filter(**{"name_reverse" + lookup: value}).values_list("pk", flat=True)
# ).distinct()
#
# related_relations_to_hit_list = []
#
# for relation_class in queryset.model.get_related_relation_classes():
#
# related_entity_classA = relation_class.get_related_entity_classA()
# related_entity_classB = relation_class.get_related_entity_classB()
# related_entity_field_nameA = relation_class.get_related_entity_field_nameA()
# related_entity_field_nameB = relation_class.get_related_entity_field_nameB()
#
# if queryset.model == related_entity_classA:
#
# # Only difference to method 'related_entity_name_method' is that the lookup is done on 'relation_type_id'
# related_relations_to_hit_list.append(
# relation_class.objects.filter(
# **{"relation_type_id__in": property_hit}
# ).values_list(related_entity_field_nameA + "_id", flat=True)
# )
#
# if queryset.model == related_entity_classB:
#
# # Only difference to method 'related_entity_name_method' is that the lookup is done on 'relation_type_id'
# related_relations_to_hit_list.append(
# relation_class.objects.filter(
# **{"relation_type_id__in": property_hit}
# ).values_list(related_entity_field_nameB + "_id", flat=True)
# )
#
# if queryset.model != related_entity_classA and queryset.model != related_entity_classB:
#
# raise ValueError("queryset model class has a wrong relation class associated!")
#
# queryset_filtered_list = [ queryset.filter(pk__in=related_relation) for related_relation in related_relations_to_hit_list ]
#
# result = reduce( lambda a,b : a | b, queryset_filtered_list).distinct()
#
# return result
#
# __after_rdf_refactoring__
lookup, value = self.construct_lookup(value)

queryset = queryset.filter(
Q(**{f"triple_set_from_obj__prop__name{lookup}": value})
| Q(**{f"triple_set_from_obj__prop__name_reverse{lookup}": value})
| Q(**{f"triple_set_from_subj__prop__name{lookup}": value})
| Q(**{f"triple_set_from_subj__prop__name_reverse{lookup}": value})
).distinct()

return queryset

def related_arbitrary_model_name(self, queryset, name, value):
"""
Searches through an arbitrarily related model on its name field.
Note that this works only if
* the related model has a field 'name'
* the filter using this method has the same name as the field of the model on which the filter is applied.
(E.g. the field 'profession' on a person relates to another model: the professiontype. Here the filter on a person
must also be called 'profession' as the field 'profession' exists within the person model and is then used to search in.
Using this example of professions, such a lookup would be generated: Person.objects.filter(profession__name__... ) )
"""

lookup, value = self.construct_lookup(value)

# name variable is the name of the filter and needs the corresponding field within the model
return queryset.filter(**{name + "__name" + lookup: value})


#######################################################################
#
Expand Down
100 changes: 100 additions & 0 deletions apis_core/utils/filtermethods.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
"""
This module contains filter functions that can be used by django-filter filters
See https://django-filter.readthedocs.io/en/main/ref/filters.html#method
"""
from django.db.models import Q


# should return tuple[str, str] once we are >=3.9
def construct_lookup(value: str) -> tuple:
"""
Helper method to parse input values and construct field lookups
(https://docs.djangoproject.com/en/4.2/ref/models/querysets/#field-lookups)
Parses user input for wildcards and returns a tuple containing the
interpreted django lookup string and the trimmed value
E.g.
- ``example`` -> ``('__icontains', 'example')``
- ``*example*`` -> ``('__icontains', 'example')``
- ``*example`` -> ``('__iendswith', 'example')``
- ``example*``-> ``('__istartswith', 'example')``
- ``"example"`` -> ``('__iexact', 'example')``
:param str value: text to be parsed for ``*``
:return: a tuple containing the lookup type and the value without modifiers
"""

if value.startswith("*") and not value.endswith("*"):
value = value[1:]
return "__iendswith", value

elif not value.startswith("*") and value.endswith("*"):
value = value[:-1]
return "__istartswith", value

elif value.startswith('"') and value.endswith('"'):
value = value[1:-1]
return "__iexact", value

else:
if value.startswith("*") and value.endswith("*"):
value = value[1:-1]
return "__icontains", value


def flexible_string(queryset, name, value):
"""
filter method for filtering arbitrary fields using :func:`construct_lookup`
"""
lookup, value = construct_lookup(value)
return queryset.filter(**{name + lookup: value})


# filtermethods for specific usecases:

# TODO RDF: Check if this should be removed or adapted
def related_entity_name(queryset, name, value):
"""
filter on the name of a related entity using :func:`construct_lookup`
"""
lookup, value = construct_lookup(value)

queryset = queryset.filter(
Q(**{f"triple_set_from_obj__subj__name{lookup}": value})
| Q(**{f"triple_set_from_subj__obj__name{lookup}": value})
).distinct()

return queryset


def related_property_name(queryset, name, value):
"""
filter on the name of a related property using :func:`construct_lookup`
"""
lookup, value = construct_lookup(value)

queryset = queryset.filter(
Q(**{f"triple_set_from_obj__prop__name{lookup}": value})
| Q(**{f"triple_set_from_obj__prop__name_reverse{lookup}": value})
| Q(**{f"triple_set_from_subj__prop__name{lookup}": value})
| Q(**{f"triple_set_from_subj__prop__name_reverse{lookup}": value})
).distinct()

return queryset


def related_arbitrary_model_name(queryset, name, value):
"""
Searches through an arbitrarily related model on its name field using :func:`construct_lookup`.
Note that this works only if
- the related model has a field 'name'
- the filter using this method has the same name as the field of the model on which the filter is applied.
(E.g. the field ``profession`` on a person relates to another model: the professiontype. Here the filter on a person
must also be called ``profession`` as the field ``profession`` exists within the person model and is then used to search in.
Using this example of professions, such a lookup would be generated: ``Person.objects.filter(profession__name__... )`` )
"""

lookup, value = self.construct_lookup(value)

# name variable is the name of the filter and needs the corresponding field within the model
return queryset.filter(**{name + "__name" + lookup: value})

0 comments on commit ac9fa86

Please sign in to comment.