@@ -245,15 +245,19 @@ def filter_queryset(self, queryset):
245245 `empty_value` to any filters.
246246 """
247247 for name , value in self .form .cleaned_data .items ():
248- field_name = self .filters [name ].field_name
249- if value == self .empty_value :
248+ filter_field = self .filters [name ]
249+ field_name = filter_field .field_name
250+
251+ if isinstance (filter_field , QuerySearchFilter ):
252+ queryset = filter_field .filter (queryset , value )
253+ elif value == self .empty_value :
250254 queryset = queryset .filter (** {f"{ field_name } __in" : EMPTY_VALUES })
251255 elif value == self .any_value :
252256 queryset = queryset .filter (~ Q (** {f"{ field_name } __in" : EMPTY_VALUES }))
253257 elif value == self .other_value and hasattr (queryset , "less_common" ):
254258 return queryset .less_common (name )
255259 else :
256- queryset = self . filters [ name ] .filter (queryset , value )
260+ queryset = filter_field .filter (queryset , value )
257261
258262 return queryset
259263
@@ -266,7 +270,7 @@ def filter_for_lookup(cls, field, lookup_type):
266270 return super ().filter_for_lookup (field , lookup_type )
267271
268272
269- def parse_query_string_to_lookups (query_string , default_lookup_expr , default_field ):
273+ def parse_query_string_to_lookups (query_string , default_lookup_expr , search_fields ):
270274 """Parse a query string and convert it into queryset lookups using Q objects."""
271275 lookups = Q ()
272276 terms = shlex .split (query_string )
@@ -295,11 +299,14 @@ def parse_query_string_to_lookups(query_string, default_lookup_expr, default_fie
295299 field_name = field_name [1 :]
296300 negated = True
297301
302+ lookups &= Q (
303+ ** {f"{ field_name } __{ lookup_expr } " : search_value }, _negated = negated
304+ )
305+
298306 else :
299307 search_value = term
300- field_name = default_field
301-
302- lookups &= Q (** {f"{ field_name } __{ lookup_expr } " : search_value }, _negated = negated )
308+ for field_name in search_fields :
309+ lookups |= Q (** {f"{ field_name } __{ lookup_expr } " : search_value })
303310
304311 return lookups
305312
@@ -323,18 +330,22 @@ class QuerySearchFilter(django_filters.CharFilter):
323330
324331 field_class = QuerySearchField
325332
333+ def __init__ (self , search_fields = None , lookup_expr = "icontains" , * args , ** kwargs ):
334+ super ().__init__ (lookup_expr = lookup_expr , * args , ** kwargs )
335+ self .search_fields = search_fields or []
336+
326337 def filter (self , qs , value ):
327338 if not value :
328339 return qs
329340
330341 lookups = parse_query_string_to_lookups (
331342 query_string = value ,
332343 default_lookup_expr = self .lookup_expr ,
333- default_field = self .field_name ,
344+ search_fields = self .search_fields ,
334345 )
335346
336347 try :
337- return qs .filter (lookups )
348+ return qs .filter (lookups ). distinct ()
338349 except FieldError :
339350 return qs .none ()
340351
@@ -347,7 +358,7 @@ class ProjectFilterSet(FilterSetUtilsMixin, django_filters.FilterSet):
347358 ]
348359
349360 search = QuerySearchFilter (
350- label = "Search" , field_name = "name" , lookup_expr = "icontains"
361+ label = "Search" , search_fields = [ "name" , "labels__name" ] , lookup_expr = "icontains"
351362 )
352363 sort = django_filters .OrderingFilter (
353364 label = "Sort" ,
@@ -412,8 +423,10 @@ def __init__(self, data=None, *args, **kwargs):
412423 if not data or data .get ("is_archived" , "" ) == "" :
413424 self .queryset = self .queryset .filter (is_archived = False )
414425
415- active_count = Project .objects .filter (is_archived = False ).count ()
416- archived_count = Project .objects .filter (is_archived = True ).count ()
426+ counts = Project .objects .get_active_archived_counts ()
427+ active_count = counts ["active_count" ]
428+ archived_count = counts ["archived_count" ]
429+
417430 self .filters ["is_archived" ].extra ["widget" ] = BulmaLinkWidget (
418431 choices = [
419432 ("" , f'<i class="fa-solid fa-seedling"></i> { active_count } Active' ),
@@ -508,7 +521,7 @@ class ResourceFilterSet(FilterSetUtilsMixin, django_filters.FilterSet):
508521
509522 search = QuerySearchFilter (
510523 label = "Search" ,
511- field_name = "path" ,
524+ search_fields = [ "path" ] ,
512525 lookup_expr = "icontains" ,
513526 )
514527 sort = django_filters .OrderingFilter (
@@ -615,15 +628,7 @@ def filter(self, qs, value):
615628 if value .startswith ("pkg:" ):
616629 return qs .for_package_url (value )
617630
618- if ":" in value :
619- return super ().filter (qs , value )
620-
621- search_fields = ["type" , "namespace" , "name" , "version" ]
622- lookups = Q ()
623- for field_names in search_fields :
624- lookups |= Q (** {f"{ field_names } __{ self .lookup_expr } " : value })
625-
626- return qs .filter (lookups )
631+ return super ().filter (qs , value )
627632
628633
629634class GroupOrderingFilter (django_filters .OrderingFilter ):
@@ -662,7 +667,9 @@ class PackageFilterSet(FilterSetUtilsMixin, django_filters.FilterSet):
662667 ]
663668
664669 search = DiscoveredPackageSearchFilter (
665- label = "Search" , field_name = "name" , lookup_expr = "icontains"
670+ label = "Search" ,
671+ search_fields = ["type" , "namespace" , "name" , "version" ],
672+ lookup_expr = "icontains" ,
666673 )
667674 sort = GroupOrderingFilter (
668675 label = "Sort" ,
@@ -746,7 +753,7 @@ class DependencyFilterSet(FilterSetUtilsMixin, django_filters.FilterSet):
746753 ]
747754
748755 search = QuerySearchFilter (
749- label = "Search" , field_name = "name" , lookup_expr = "icontains"
756+ label = "Search" , search_fields = [ "name" ] , lookup_expr = "icontains"
750757 )
751758 sort = GroupOrderingFilter (
752759 label = "Sort" ,
@@ -803,7 +810,7 @@ class Meta:
803810
804811class ProjectMessageFilterSet (FilterSetUtilsMixin , django_filters .FilterSet ):
805812 search = QuerySearchFilter (
806- label = "Search" , field_name = "description" , lookup_expr = "icontains"
813+ label = "Search" , search_fields = [ "description" ] , lookup_expr = "icontains"
807814 )
808815 sort = django_filters .OrderingFilter (
809816 label = "Sort" ,
@@ -855,7 +862,7 @@ class RelationFilterSet(FilterSetUtilsMixin, django_filters.FilterSet):
855862
856863 search = QuerySearchFilter (
857864 label = "Search" ,
858- field_name = "to_resource__path" ,
865+ search_fields = [ "to_resource__path" ] ,
859866 lookup_expr = "icontains" ,
860867 )
861868 sort = django_filters .OrderingFilter (
0 commit comments