Skip to content

Commit cf651f1

Browse files
authored
Add labels to Project level search #1520 (#1522)
Signed-off-by: tdruez <tdruez@nexb.com>
1 parent 3a40cc1 commit cf651f1

File tree

7 files changed

+48
-34
lines changed

7 files changed

+48
-34
lines changed

CHANGELOG.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ v34.9.4 (unreleased)
2727
- Add a download action on project list to enable bulk download of Project output files.
2828
https://github.com/aboutcode-org/scancode.io/issues/1518
2929

30+
- Add labels to Project level search.
31+
The labels are now always presented in alphabetical order for consistency.
32+
https://github.com/aboutcode-org/scancode.io/issues/1520
33+
3034
v34.9.3 (2024-12-31)
3135
--------------------
3236

scanpipe/filters.py

Lines changed: 29 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -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",
@@ -508,7 +519,7 @@ class ResourceFilterSet(FilterSetUtilsMixin, django_filters.FilterSet):
508519

509520
search = QuerySearchFilter(
510521
label="Search",
511-
field_name="path",
522+
search_fields=["path"],
512523
lookup_expr="icontains",
513524
)
514525
sort = django_filters.OrderingFilter(
@@ -615,15 +626,7 @@ def filter(self, qs, value):
615626
if value.startswith("pkg:"):
616627
return qs.for_package_url(value)
617628

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)
629+
return super().filter(qs, value)
627630

628631

629632
class GroupOrderingFilter(django_filters.OrderingFilter):
@@ -662,7 +665,9 @@ class PackageFilterSet(FilterSetUtilsMixin, django_filters.FilterSet):
662665
]
663666

664667
search = DiscoveredPackageSearchFilter(
665-
label="Search", field_name="name", lookup_expr="icontains"
668+
label="Search",
669+
search_fields=["type", "namespace", "name", "version"],
670+
lookup_expr="icontains",
666671
)
667672
sort = GroupOrderingFilter(
668673
label="Sort",
@@ -746,7 +751,7 @@ class DependencyFilterSet(FilterSetUtilsMixin, django_filters.FilterSet):
746751
]
747752

748753
search = QuerySearchFilter(
749-
label="Search", field_name="name", lookup_expr="icontains"
754+
label="Search", search_fields=["name"], lookup_expr="icontains"
750755
)
751756
sort = GroupOrderingFilter(
752757
label="Sort",
@@ -803,7 +808,7 @@ class Meta:
803808

804809
class ProjectMessageFilterSet(FilterSetUtilsMixin, django_filters.FilterSet):
805810
search = QuerySearchFilter(
806-
label="Search", field_name="description", lookup_expr="icontains"
811+
label="Search", search_fields=["description"], lookup_expr="icontains"
807812
)
808813
sort = django_filters.OrderingFilter(
809814
label="Sort",
@@ -855,7 +860,7 @@ class RelationFilterSet(FilterSetUtilsMixin, django_filters.FilterSet):
855860

856861
search = QuerySearchFilter(
857862
label="Search",
858-
field_name="to_resource__path",
863+
search_fields=["to_resource__path"],
859864
lookup_expr="icontains",
860865
)
861866
sort = django_filters.OrderingFilter(

scanpipe/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -561,7 +561,7 @@ class Project(UUIDPKModel, ExtraDataFieldMixin, UpdateMixin, models.Model):
561561
)
562562
notes = models.TextField(blank=True)
563563
settings = models.JSONField(default=dict, blank=True)
564-
labels = TaggableManager(through=UUIDTaggedItem)
564+
labels = TaggableManager(through=UUIDTaggedItem, ordering=["name"])
565565
purl = models.CharField(
566566
max_length=2048,
567567
blank=True,

scanpipe/tests/test_api.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -489,13 +489,13 @@ def test_scanpipe_api_project_create_pipeline_old_name_compatibility(self):
489489
def test_scanpipe_api_project_create_labels(self):
490490
data = {
491491
"name": "Project1",
492-
"labels": ["label1", "label2"],
492+
"labels": ["label2", "label1"],
493493
}
494494
response = self.csrf_client.post(self.project_list_url, data)
495495
self.assertEqual(status.HTTP_201_CREATED, response.status_code)
496-
self.assertEqual(data["labels"], sorted(response.data["labels"]))
496+
self.assertEqual(sorted(data["labels"]), response.data["labels"])
497497
project = Project.objects.get(name=data["name"])
498-
self.assertEqual(data["labels"], sorted(project.labels.names()))
498+
self.assertEqual(sorted(data["labels"]), list(project.labels.names()))
499499

500500
def test_scanpipe_api_project_create_pipeline_groups(self):
501501
data = {

scanpipe/tests/test_filters.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,11 @@ def test_scanpipe_filters_project_filterset_labels(self):
9595
filterset = ProjectFilterSet(data={"label": "label2"})
9696
self.assertEqual(0, len(filterset.qs))
9797

98+
filterset = ProjectFilterSet(data={"search": "label1"})
99+
self.assertEqual([self.project1], list(filterset.qs))
100+
filterset = ProjectFilterSet(data={"search": "lab"})
101+
self.assertEqual([self.project1], list(filterset.qs))
102+
98103
def test_scanpipe_filters_filter_queryset_empty_values(self):
99104
resource1 = CodebaseResource.objects.create(
100105
project=self.project1,
@@ -276,7 +281,7 @@ def test_scanpipe_filters_parse_query_string_to_lookups(self):
276281
inputs = {
277282
"LICENSE": "(AND: ('name__icontains', 'LICENSE'))",
278283
"two words": (
279-
"(AND: ('name__icontains', 'two'), ('name__icontains', 'words'))"
284+
"(OR: ('name__icontains', 'two'), ('name__icontains', 'words'))"
280285
),
281286
"'two words'": "(AND: ('name__icontains', 'two words'))",
282287
"na me:LICENSE": (
@@ -306,7 +311,7 @@ def test_scanpipe_filters_parse_query_string_to_lookups(self):
306311
}
307312

308313
for query_string, expected in inputs.items():
309-
lookups = parse_query_string_to_lookups(query_string, "icontains", "name")
314+
lookups = parse_query_string_to_lookups(query_string, "icontains", ["name"])
310315
self.assertEqual(expected, str(lookups))
311316

312317
def test_scanpipe_filters_filter_advanced_search_query_string(self):

scanpipe/tests/test_models.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -816,13 +816,13 @@ def test_scanpipe_project_get_ignored_vulnerabilities_set(self):
816816
self.assertEqual(expected, self.project1.get_ignored_vulnerabilities_set())
817817

818818
def test_scanpipe_project_model_labels(self):
819-
self.project1.labels.add("label1", "label2")
819+
self.project1.labels.add("label2", "label1")
820820
self.assertEqual(2, UUIDTaggedItem.objects.count())
821-
self.assertEqual(["label1", "label2"], sorted(self.project1.labels.names()))
821+
self.assertEqual(["label1", "label2"], list(self.project1.labels.names()))
822822

823823
self.project1.labels.remove("label1")
824824
self.assertEqual(1, UUIDTaggedItem.objects.count())
825-
self.assertEqual(["label2"], sorted(self.project1.labels.names()))
825+
self.assertEqual(["label2"], list(self.project1.labels.names()))
826826

827827
self.project1.labels.clear()
828828
self.assertEqual(0, UUIDTaggedItem.objects.count())

scanpipe/tests/test_views.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,7 @@ def test_scanpipe_views_project_details_add_labels(self):
358358
data["add-labels-submit"] = ""
359359
response = self.client.post(url, data, follow=True)
360360
self.assertContains(response, "Label(s) added.")
361-
self.assertEqual(["label1", "label2"], sorted(self.project1.labels.names()))
361+
self.assertEqual(["label1", "label2"], list(self.project1.labels.names()))
362362

363363
def test_scanpipe_views_project_delete_label(self):
364364
self.project1.labels.add("label1")

0 commit comments

Comments
 (0)