Skip to content

Commit 0eb00e7

Browse files
authored
Enhance list view filters #216 (#791)
Signed-off-by: Thomas Druez <tdruez@nexb.com>
1 parent b577209 commit 0eb00e7

17 files changed

Lines changed: 196 additions & 88 deletions

CHANGELOG.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ Changelog
44
v33.0.0 (unreleased)
55
--------------------
66

7+
- Add multiple new filtering option to list views table headers.
8+
Refactored the way to define filters using the table_columns view attribute.
9+
https://github.com/nexB/scancode.io/issues/216
10+
https://github.com/nexB/scancode.io/issues/580
11+
https://github.com/nexB/scancode.io/issues/506
12+
713
- Update the CycloneDX BOM download file extension from ``.bom.json`` to ``.cdx.json``.
814
https://github.com/nexB/scancode.io/issues/785
915

scancodeio/settings.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,10 @@
160160

161161
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
162162

163+
# Forms and filters
164+
165+
FILTERS_EMPTY_CHOICE_LABEL = env.str("FILTERS_EMPTY_CHOICE_LABEL", default="All")
166+
163167
# Templates
164168

165169
TEMPLATES = [

scanpipe/filters.py

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from django.apps import apps
2424
from django.core.validators import EMPTY_VALUES
2525
from django.db import models
26+
from django.db.models import Q
2627
from django.db.models.fields import BLANK_CHOICE_DASH
2728
from django.utils.http import urlencode
2829
from django.utils.translation import gettext as _
@@ -41,11 +42,15 @@
4142
scanpipe_app = apps.get_app_config("scanpipe")
4243

4344
PAGE_VAR = "page"
45+
EMPTY_VAR = "_EMPTY_"
46+
ANY_VAR = "_ANY_"
47+
OTHER_VAR = "_OTHER_"
4448

4549

4650
class FilterSetUtilsMixin:
47-
empty_value = "_EMPTY_"
48-
other_value = "_OTHER_"
51+
empty_value = EMPTY_VAR
52+
any_value = ANY_VAR
53+
other_value = OTHER_VAR
4954

5055
@staticmethod
5156
def remove_field_from_query_dict(query_dict, field_name, remove_value=None):
@@ -122,6 +127,8 @@ def filter_queryset(self, queryset):
122127
field_name = self.filters[name].field_name
123128
if value == self.empty_value:
124129
queryset = queryset.filter(**{f"{field_name}__in": EMPTY_VALUES})
130+
elif value == self.any_value:
131+
queryset = queryset.filter(~Q(**{f"{field_name}__in": EMPTY_VALUES}))
125132
elif value == self.other_value and hasattr(queryset, "less_common"):
126133
return queryset.less_common(name)
127134
else:
@@ -170,6 +177,16 @@ class BulmaDropdownWidget(BulmaLinkWidget):
170177
extra_css_class = "dropdown-item"
171178

172179

180+
class HasValueDropdownWidget(BulmaDropdownWidget):
181+
def __init__(self, attrs=None, choices=()):
182+
super().__init__(attrs)
183+
self.choices = (
184+
("", "All"),
185+
(EMPTY_VAR, "None"),
186+
(ANY_VAR, "Any"),
187+
)
188+
189+
173190
class ProjectFilterSet(FilterSetUtilsMixin, django_filters.FilterSet):
174191
search = django_filters.CharFilter(
175192
label="Search", field_name="name", lookup_expr="icontains"
@@ -268,8 +285,8 @@ def filter(self, qs, value):
268285
class InPackageFilter(django_filters.ChoiceFilter):
269286
def __init__(self, *args, **kwargs):
270287
kwargs["choices"] = (
271-
("true", "Yes"),
272-
("false", "No"),
288+
("true", "In a package"),
289+
("false", "Not in a package"),
273290
)
274291
super().__init__(*args, **kwargs)
275292

@@ -314,6 +331,14 @@ def filter(self, qs, value):
314331

315332

316333
class ResourceFilterSet(FilterSetUtilsMixin, django_filters.FilterSet):
334+
dropdown_widget = [
335+
"status",
336+
"type",
337+
"compliance_alert",
338+
"in_package",
339+
"relation_map_type",
340+
]
341+
317342
search = django_filters.CharFilter(
318343
label="Search",
319344
field_name="path",
@@ -338,14 +363,13 @@ class ResourceFilterSet(FilterSetUtilsMixin, django_filters.FilterSet):
338363
],
339364
)
340365
compliance_alert = django_filters.ChoiceFilter(
341-
choices=CodebaseResource.Compliance.choices + [("_EMPTY_", "EMPTY")]
366+
choices=[(EMPTY_VAR, "None")] + CodebaseResource.Compliance.choices,
342367
)
343-
in_package = InPackageFilter(label="In a Package")
344-
status = StatusFilter(empty_label="All")
368+
in_package = InPackageFilter(label="In a package")
369+
status = StatusFilter()
345370
relation_map_type = RelationMapTypeFilter(
346371
label="Relation map type",
347372
field_name="related_from__map_type",
348-
empty_label="All",
349373
)
350374

351375
class Meta:
@@ -387,9 +411,16 @@ def __init__(self, *args, **kwargs):
387411
if status_filter := self.filters.get("status"):
388412
status_filter.extra.update({"choices": self.get_status_choices()})
389413

414+
# Set the `BulmaDropdownWidget`` widget for defined ``dropdown_widget``.
415+
for field_name in self.dropdown_widget:
416+
self.filters[field_name].extra["widget"] = BulmaDropdownWidget()
417+
418+
license_expression_filer = self.filters["detected_license_expression"]
419+
license_expression_filer.extra["widget"] = HasValueDropdownWidget()
420+
390421
def get_status_choices(self):
391422
default_choices = [
392-
("_EMPTY_", "No status"),
423+
(EMPTY_VAR, "No status"),
393424
("any", "Any status"),
394425
]
395426
status_values = (
@@ -446,11 +477,19 @@ class Meta:
446477
"vcs_url",
447478
"type",
448479
"declared_license_expression",
480+
"declared_license_expression_spdx",
449481
"other_license_expression",
482+
"other_license_expression_spdx",
450483
"extracted_license_statement",
451484
"copyright",
452485
]
453486

487+
def __init__(self, *args, **kwargs):
488+
super().__init__(*args, **kwargs)
489+
license_expression_filer = self.filters["declared_license_expression"]
490+
license_expression_filer.extra["widget"] = HasValueDropdownWidget()
491+
self.filters["copyright"].extra["widget"] = HasValueDropdownWidget()
492+
454493

455494
class DependencyFilterSet(FilterSetUtilsMixin, django_filters.FilterSet):
456495
search = django_filters.CharFilter(

scanpipe/templates/scanpipe/base.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@
6868
#project-extra-data .is-more {position: relative;}
6969
#project-extra-data .is-more-show {bottom: -1.25em; height: 2.5em; left: calc(50% - 4.5em); padding: 0; position: absolute; width: 9em; z-index: 1; background-color: #ff9970; border: none; border-radius: 0.5em!important;}
7070
#project-extra-data pre {background-color: initial; color: initial; padding: initial; white-space: pre-wrap; word-break: break-all;}
71+
#codebase-relation-list table {table-layout: fixed;}
72+
#codebase-relation-list th#column-status {width: 110px;}
73+
#codebase-relation-list th#column-related_from__map_type {width: 145px;}
7174
</style>
7275
{% block extrahead %}{% endblock %}
7376
</head>

scanpipe/templates/scanpipe/dependency_list.html

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,22 +25,22 @@
2525
<a href="{{ dependency.get_absolute_url }}">{{ dependency.purl }}</a>
2626
</td>
2727
<td>
28-
{{ dependency.type }}
28+
<a href="?type={{ dependency.type }}" class="is-black-link">{{ dependency.type }}</a>
2929
</td>
3030
<td>
3131
{{ dependency.extracted_requirement }}
3232
</td>
3333
<td class="break-normal">
34-
{{ dependency.scope }}
34+
<a href="?scope={{ dependency.scope }}" class="is-black-link">{{ dependency.scope }}</a>
3535
</td>
3636
<td>
37-
{{ dependency.is_runtime }}
37+
<a href="?is_runtime={{ dependency.is_runtime }}" class="is-black-link">{{ dependency.is_runtime }}</a>
3838
</td>
3939
<td>
40-
{{ dependency.is_optional }}
40+
<a href="?is_optional={{ dependency.is_optional }}" class="is-black-link">{{ dependency.is_optional }}</a>
4141
</td>
4242
<td>
43-
{{ dependency.is_resolved }}
43+
<a href="?is_resolved={{ dependency.is_resolved }}" class="is-black-link">{{ dependency.is_resolved }}</a>
4444
</td>
4545
<td>
4646
{% if dependency.for_package %}
@@ -56,6 +56,12 @@
5656
{{ dependency.datasource_id }}
5757
</td>
5858
</tr>
59+
{% empty %}
60+
<tr>
61+
<td colspan="42" class="has-text-centered p-3">
62+
No Dependencies found. <a href="?">Clear search and filters</a>
63+
</td>
64+
</tr>
5965
{% endfor %}
6066
</tbody>
6167
</table>

scanpipe/templates/scanpipe/error_list.html

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@
5252
<pre class="log wrap p-0" style="max-height: 150px;"><code>{{ error.traceback }}</code></pre>
5353
</td>
5454
</tr>
55+
{% empty %}
56+
<tr>
57+
<td colspan="42" class="has-text-centered p-3">
58+
No Errors found. <a href="?">Clear search and filters</a>
59+
</td>
60+
</tr>
5561
{% endfor %}
5662
</tbody>
5763
</table>

scanpipe/templates/scanpipe/includes/file_filter.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,17 @@
22
<div class="tabs is-toggle is-toggle-rounded is-centered">
33
<ul>
44
<li{% if file_filter == "all" %} class="is-active"{% endif %}>
5-
<a href="?file-filter=all#resource-charts">
5+
<a href="?file-filter=all#charts">
66
All Files <span class="tag is-link is-light is-rounded ml-1">{{ project.file_count|intcomma }}</span>
77
</a>
88
</li>
99
<li{% if file_filter == "in-a-package" %} class="is-active"{% endif %}>
10-
<a href="?file-filter=in-a-package#resource-charts">
10+
<a href="?file-filter=in-a-package#charts">
1111
In a Package <span class="tag is-link is-light is-rounded ml-1">{{ project.file_in_package_count|intcomma }}</span>
1212
</a>
1313
</li>
1414
<li{% if file_filter == "not-in-a-package" %} class="is-active"{% endif %}>
15-
<a href="?file-filter=not-in-a-package#resource-charts">
15+
<a href="?file-filter=not-in-a-package#charts">
1616
NOT in a Package <span class="tag is-link is-light is-rounded ml-1">{{ project.file_not_in_package_count|intcomma }}</span>
1717
</a>
1818
</li>
Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,12 @@
1-
<div class="dropdown is-hoverable">
1+
<div class="dropdown is-hoverable {% if is_right %}is-right{% endif %}">
22
<div class="dropdown-trigger">
3-
<a aria-haspopup="true" aria-controls="{{ filter_form_field.id_for_label }}">
3+
<a class="{% if filter.data %}has-text-link{% else %}is-grey-link{% endif %}" aria-haspopup="true" aria-controls="{{ filter.id_for_label }}">
44
<i class="fa-solid fa-filter"></i>
55
</a>
66
</div>
7-
<div class="dropdown-menu" id="{{ filter_form_field.id_for_label }}" role="menu">
7+
<div class="dropdown-menu" id="{{ filter.id_for_label }}" role="menu">
88
<div class="dropdown-content">
9-
{% for value, label in filter_form_field.field.choices %}
10-
<a href="?{{ filter_form_field.html_name }}={{ value }}" class="dropdown-item">
11-
{{ label }}
12-
</a>
13-
{% endfor %}
9+
{{ filter }}
1410
</div>
1511
</div>
1612
</div>
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
<a href="?sort={% if "-" not in filter.data.sort %}-{% endif %}{{ field_name }}" class="is-black-link">
2-
{{ label }}
1+
<a href="?{{ column.sort_query }}" class="is-black-link">
2+
{{ column.label }}
33
</a>
4-
{% if field_name in filter.data.sort %}
5-
<i class="fa-solid fa-sort-{% if "-" in filter.data.sort %}down{% else %}up{% endif %}"></i>
4+
{% if column.is_sorted %}
5+
<i class="fa-solid fa-sort-{% if column.sort_direction == "-" %}down{% else %}up{% endif %}"></i>
66
{% endif %}

scanpipe/templates/scanpipe/includes/list_actions_dropdown.html

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
<div class="dropdown is-hoverable">
22
<div class="dropdown-trigger">
33
<button class="button is-success is-small mr-1" aria-haspopup="true" aria-controls="dropdown-menu-action">
4-
<i class="fa-solid fa-download mr-2"></i> Export
4+
<span class="icon mr-1">
5+
<i class="fa-solid fa-download"></i>
6+
</span>
7+
Export
58
</button>
69
</div>
710
<div class="dropdown-menu" id="dropdown-menu-action" role="menu">

0 commit comments

Comments
 (0)