Skip to content

Commit

Permalink
Add labels to Project level search #1520 (#1522)
Browse files Browse the repository at this point in the history
Signed-off-by: tdruez <[email protected]>
  • Loading branch information
tdruez authored Jan 8, 2025
1 parent 3a40cc1 commit cf651f1
Show file tree
Hide file tree
Showing 7 changed files with 48 additions and 34 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ v34.9.4 (unreleased)
- Add a download action on project list to enable bulk download of Project output files.
https://github.com/aboutcode-org/scancode.io/issues/1518

- Add labels to Project level search.
The labels are now always presented in alphabetical order for consistency.
https://github.com/aboutcode-org/scancode.io/issues/1520

v34.9.3 (2024-12-31)
--------------------

Expand Down
53 changes: 29 additions & 24 deletions scanpipe/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,15 +245,19 @@ def filter_queryset(self, queryset):
`empty_value` to any filters.
"""
for name, value in self.form.cleaned_data.items():
field_name = self.filters[name].field_name
if value == self.empty_value:
filter_field = self.filters[name]
field_name = filter_field.field_name

if isinstance(filter_field, QuerySearchFilter):
queryset = filter_field.filter(queryset, value)
elif value == self.empty_value:
queryset = queryset.filter(**{f"{field_name}__in": EMPTY_VALUES})
elif value == self.any_value:
queryset = queryset.filter(~Q(**{f"{field_name}__in": EMPTY_VALUES}))
elif value == self.other_value and hasattr(queryset, "less_common"):
return queryset.less_common(name)
else:
queryset = self.filters[name].filter(queryset, value)
queryset = filter_field.filter(queryset, value)

return queryset

Expand All @@ -266,7 +270,7 @@ def filter_for_lookup(cls, field, lookup_type):
return super().filter_for_lookup(field, lookup_type)


def parse_query_string_to_lookups(query_string, default_lookup_expr, default_field):
def parse_query_string_to_lookups(query_string, default_lookup_expr, search_fields):
"""Parse a query string and convert it into queryset lookups using Q objects."""
lookups = Q()
terms = shlex.split(query_string)
Expand Down Expand Up @@ -295,11 +299,14 @@ def parse_query_string_to_lookups(query_string, default_lookup_expr, default_fie
field_name = field_name[1:]
negated = True

lookups &= Q(
**{f"{field_name}__{lookup_expr}": search_value}, _negated=negated
)

else:
search_value = term
field_name = default_field

lookups &= Q(**{f"{field_name}__{lookup_expr}": search_value}, _negated=negated)
for field_name in search_fields:
lookups |= Q(**{f"{field_name}__{lookup_expr}": search_value})

return lookups

Expand All @@ -323,18 +330,22 @@ class QuerySearchFilter(django_filters.CharFilter):

field_class = QuerySearchField

def __init__(self, search_fields=None, lookup_expr="icontains", *args, **kwargs):
super().__init__(lookup_expr=lookup_expr, *args, **kwargs)
self.search_fields = search_fields or []

def filter(self, qs, value):
if not value:
return qs

lookups = parse_query_string_to_lookups(
query_string=value,
default_lookup_expr=self.lookup_expr,
default_field=self.field_name,
search_fields=self.search_fields,
)

try:
return qs.filter(lookups)
return qs.filter(lookups).distinct()
except FieldError:
return qs.none()

Expand All @@ -347,7 +358,7 @@ class ProjectFilterSet(FilterSetUtilsMixin, django_filters.FilterSet):
]

search = QuerySearchFilter(
label="Search", field_name="name", lookup_expr="icontains"
label="Search", search_fields=["name", "labels__name"], lookup_expr="icontains"
)
sort = django_filters.OrderingFilter(
label="Sort",
Expand Down Expand Up @@ -508,7 +519,7 @@ class ResourceFilterSet(FilterSetUtilsMixin, django_filters.FilterSet):

search = QuerySearchFilter(
label="Search",
field_name="path",
search_fields=["path"],
lookup_expr="icontains",
)
sort = django_filters.OrderingFilter(
Expand Down Expand Up @@ -615,15 +626,7 @@ def filter(self, qs, value):
if value.startswith("pkg:"):
return qs.for_package_url(value)

if ":" in value:
return super().filter(qs, value)

search_fields = ["type", "namespace", "name", "version"]
lookups = Q()
for field_names in search_fields:
lookups |= Q(**{f"{field_names}__{self.lookup_expr}": value})

return qs.filter(lookups)
return super().filter(qs, value)


class GroupOrderingFilter(django_filters.OrderingFilter):
Expand Down Expand Up @@ -662,7 +665,9 @@ class PackageFilterSet(FilterSetUtilsMixin, django_filters.FilterSet):
]

search = DiscoveredPackageSearchFilter(
label="Search", field_name="name", lookup_expr="icontains"
label="Search",
search_fields=["type", "namespace", "name", "version"],
lookup_expr="icontains",
)
sort = GroupOrderingFilter(
label="Sort",
Expand Down Expand Up @@ -746,7 +751,7 @@ class DependencyFilterSet(FilterSetUtilsMixin, django_filters.FilterSet):
]

search = QuerySearchFilter(
label="Search", field_name="name", lookup_expr="icontains"
label="Search", search_fields=["name"], lookup_expr="icontains"
)
sort = GroupOrderingFilter(
label="Sort",
Expand Down Expand Up @@ -803,7 +808,7 @@ class Meta:

class ProjectMessageFilterSet(FilterSetUtilsMixin, django_filters.FilterSet):
search = QuerySearchFilter(
label="Search", field_name="description", lookup_expr="icontains"
label="Search", search_fields=["description"], lookup_expr="icontains"
)
sort = django_filters.OrderingFilter(
label="Sort",
Expand Down Expand Up @@ -855,7 +860,7 @@ class RelationFilterSet(FilterSetUtilsMixin, django_filters.FilterSet):

search = QuerySearchFilter(
label="Search",
field_name="to_resource__path",
search_fields=["to_resource__path"],
lookup_expr="icontains",
)
sort = django_filters.OrderingFilter(
Expand Down
2 changes: 1 addition & 1 deletion scanpipe/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -561,7 +561,7 @@ class Project(UUIDPKModel, ExtraDataFieldMixin, UpdateMixin, models.Model):
)
notes = models.TextField(blank=True)
settings = models.JSONField(default=dict, blank=True)
labels = TaggableManager(through=UUIDTaggedItem)
labels = TaggableManager(through=UUIDTaggedItem, ordering=["name"])
purl = models.CharField(
max_length=2048,
blank=True,
Expand Down
6 changes: 3 additions & 3 deletions scanpipe/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -489,13 +489,13 @@ def test_scanpipe_api_project_create_pipeline_old_name_compatibility(self):
def test_scanpipe_api_project_create_labels(self):
data = {
"name": "Project1",
"labels": ["label1", "label2"],
"labels": ["label2", "label1"],
}
response = self.csrf_client.post(self.project_list_url, data)
self.assertEqual(status.HTTP_201_CREATED, response.status_code)
self.assertEqual(data["labels"], sorted(response.data["labels"]))
self.assertEqual(sorted(data["labels"]), response.data["labels"])
project = Project.objects.get(name=data["name"])
self.assertEqual(data["labels"], sorted(project.labels.names()))
self.assertEqual(sorted(data["labels"]), list(project.labels.names()))

def test_scanpipe_api_project_create_pipeline_groups(self):
data = {
Expand Down
9 changes: 7 additions & 2 deletions scanpipe/tests/test_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,11 @@ def test_scanpipe_filters_project_filterset_labels(self):
filterset = ProjectFilterSet(data={"label": "label2"})
self.assertEqual(0, len(filterset.qs))

filterset = ProjectFilterSet(data={"search": "label1"})
self.assertEqual([self.project1], list(filterset.qs))
filterset = ProjectFilterSet(data={"search": "lab"})
self.assertEqual([self.project1], list(filterset.qs))

def test_scanpipe_filters_filter_queryset_empty_values(self):
resource1 = CodebaseResource.objects.create(
project=self.project1,
Expand Down Expand Up @@ -276,7 +281,7 @@ def test_scanpipe_filters_parse_query_string_to_lookups(self):
inputs = {
"LICENSE": "(AND: ('name__icontains', 'LICENSE'))",
"two words": (
"(AND: ('name__icontains', 'two'), ('name__icontains', 'words'))"
"(OR: ('name__icontains', 'two'), ('name__icontains', 'words'))"
),
"'two words'": "(AND: ('name__icontains', 'two words'))",
"na me:LICENSE": (
Expand Down Expand Up @@ -306,7 +311,7 @@ def test_scanpipe_filters_parse_query_string_to_lookups(self):
}

for query_string, expected in inputs.items():
lookups = parse_query_string_to_lookups(query_string, "icontains", "name")
lookups = parse_query_string_to_lookups(query_string, "icontains", ["name"])
self.assertEqual(expected, str(lookups))

def test_scanpipe_filters_filter_advanced_search_query_string(self):
Expand Down
6 changes: 3 additions & 3 deletions scanpipe/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -816,13 +816,13 @@ def test_scanpipe_project_get_ignored_vulnerabilities_set(self):
self.assertEqual(expected, self.project1.get_ignored_vulnerabilities_set())

def test_scanpipe_project_model_labels(self):
self.project1.labels.add("label1", "label2")
self.project1.labels.add("label2", "label1")
self.assertEqual(2, UUIDTaggedItem.objects.count())
self.assertEqual(["label1", "label2"], sorted(self.project1.labels.names()))
self.assertEqual(["label1", "label2"], list(self.project1.labels.names()))

self.project1.labels.remove("label1")
self.assertEqual(1, UUIDTaggedItem.objects.count())
self.assertEqual(["label2"], sorted(self.project1.labels.names()))
self.assertEqual(["label2"], list(self.project1.labels.names()))

self.project1.labels.clear()
self.assertEqual(0, UUIDTaggedItem.objects.count())
Expand Down
2 changes: 1 addition & 1 deletion scanpipe/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,7 @@ def test_scanpipe_views_project_details_add_labels(self):
data["add-labels-submit"] = ""
response = self.client.post(url, data, follow=True)
self.assertContains(response, "Label(s) added.")
self.assertEqual(["label1", "label2"], sorted(self.project1.labels.names()))
self.assertEqual(["label1", "label2"], list(self.project1.labels.names()))

def test_scanpipe_views_project_delete_label(self):
self.project1.labels.add("label1")
Expand Down

0 comments on commit cf651f1

Please sign in to comment.