Skip to content
6 changes: 6 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ Changelog
v33.0.0 (unreleased)
--------------------

- Add multiple new filtering option to list views table headers.
Refactored the way to define filters using the table_columns view attribute.
https://github.com/nexB/scancode.io/issues/216
https://github.com/nexB/scancode.io/issues/580
https://github.com/nexB/scancode.io/issues/506

- Update the CycloneDX BOM download file extension from ``.bom.json`` to ``.cdx.json``.
https://github.com/nexB/scancode.io/issues/785

Expand Down
4 changes: 4 additions & 0 deletions scancodeio/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,10 @@

DEFAULT_AUTO_FIELD = "django.db.models.AutoField"

# Forms and filters

FILTERS_EMPTY_CHOICE_LABEL = env.str("FILTERS_EMPTY_CHOICE_LABEL", default="All")

# Templates

TEMPLATES = [
Expand Down
57 changes: 48 additions & 9 deletions scanpipe/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from django.apps import apps
from django.core.validators import EMPTY_VALUES
from django.db import models
from django.db.models import Q
from django.db.models.fields import BLANK_CHOICE_DASH
from django.utils.http import urlencode
from django.utils.translation import gettext as _
Expand All @@ -41,11 +42,15 @@
scanpipe_app = apps.get_app_config("scanpipe")

PAGE_VAR = "page"
EMPTY_VAR = "_EMPTY_"
ANY_VAR = "_ANY_"
OTHER_VAR = "_OTHER_"


class FilterSetUtilsMixin:
empty_value = "_EMPTY_"
other_value = "_OTHER_"
empty_value = EMPTY_VAR
any_value = ANY_VAR
other_value = OTHER_VAR

@staticmethod
def remove_field_from_query_dict(query_dict, field_name, remove_value=None):
Expand Down Expand Up @@ -122,6 +127,8 @@ def filter_queryset(self, queryset):
field_name = self.filters[name].field_name
if 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:
Expand Down Expand Up @@ -170,6 +177,16 @@ class BulmaDropdownWidget(BulmaLinkWidget):
extra_css_class = "dropdown-item"


class HasValueDropdownWidget(BulmaDropdownWidget):
def __init__(self, attrs=None, choices=()):
super().__init__(attrs)
self.choices = (
("", "All"),
(EMPTY_VAR, "None"),
(ANY_VAR, "Any"),
)


class ProjectFilterSet(FilterSetUtilsMixin, django_filters.FilterSet):
search = django_filters.CharFilter(
label="Search", field_name="name", lookup_expr="icontains"
Expand Down Expand Up @@ -268,8 +285,8 @@ def filter(self, qs, value):
class InPackageFilter(django_filters.ChoiceFilter):
def __init__(self, *args, **kwargs):
kwargs["choices"] = (
("true", "Yes"),
("false", "No"),
("true", "In a package"),
("false", "Not in a package"),
)
super().__init__(*args, **kwargs)

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


class ResourceFilterSet(FilterSetUtilsMixin, django_filters.FilterSet):
dropdown_widget = [
"status",
"type",
"compliance_alert",
"in_package",
"relation_map_type",
]

search = django_filters.CharFilter(
label="Search",
field_name="path",
Expand All @@ -338,14 +363,13 @@ class ResourceFilterSet(FilterSetUtilsMixin, django_filters.FilterSet):
],
)
compliance_alert = django_filters.ChoiceFilter(
choices=CodebaseResource.Compliance.choices + [("_EMPTY_", "EMPTY")]
choices=[(EMPTY_VAR, "None")] + CodebaseResource.Compliance.choices,
)
in_package = InPackageFilter(label="In a Package")
status = StatusFilter(empty_label="All")
in_package = InPackageFilter(label="In a package")
status = StatusFilter()
relation_map_type = RelationMapTypeFilter(
label="Relation map type",
field_name="related_from__map_type",
empty_label="All",
)

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

# Set the `BulmaDropdownWidget`` widget for defined ``dropdown_widget``.
for field_name in self.dropdown_widget:
self.filters[field_name].extra["widget"] = BulmaDropdownWidget()

license_expression_filer = self.filters["detected_license_expression"]
license_expression_filer.extra["widget"] = HasValueDropdownWidget()

def get_status_choices(self):
default_choices = [
("_EMPTY_", "No status"),
(EMPTY_VAR, "No status"),
("any", "Any status"),
]
status_values = (
Expand Down Expand Up @@ -446,11 +477,19 @@ class Meta:
"vcs_url",
"type",
"declared_license_expression",
"declared_license_expression_spdx",
"other_license_expression",
"other_license_expression_spdx",
"extracted_license_statement",
"copyright",
]

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
license_expression_filer = self.filters["declared_license_expression"]
license_expression_filer.extra["widget"] = HasValueDropdownWidget()
self.filters["copyright"].extra["widget"] = HasValueDropdownWidget()


class DependencyFilterSet(FilterSetUtilsMixin, django_filters.FilterSet):
search = django_filters.CharFilter(
Expand Down
3 changes: 3 additions & 0 deletions scanpipe/templates/scanpipe/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@
#project-extra-data .is-more {position: relative;}
#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;}
#project-extra-data pre {background-color: initial; color: initial; padding: initial; white-space: pre-wrap; word-break: break-all;}
#codebase-relation-list table {table-layout: fixed;}
#codebase-relation-list th#column-status {width: 110px;}
#codebase-relation-list th#column-related_from__map_type {width: 145px;}
</style>
{% block extrahead %}{% endblock %}
</head>
Expand Down
16 changes: 11 additions & 5 deletions scanpipe/templates/scanpipe/dependency_list.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,22 +25,22 @@
<a href="{{ dependency.get_absolute_url }}">{{ dependency.purl }}</a>
</td>
<td>
{{ dependency.type }}
<a href="?type={{ dependency.type }}" class="is-black-link">{{ dependency.type }}</a>
</td>
<td>
{{ dependency.extracted_requirement }}
</td>
<td class="break-normal">
{{ dependency.scope }}
<a href="?scope={{ dependency.scope }}" class="is-black-link">{{ dependency.scope }}</a>
</td>
<td>
{{ dependency.is_runtime }}
<a href="?is_runtime={{ dependency.is_runtime }}" class="is-black-link">{{ dependency.is_runtime }}</a>
</td>
<td>
{{ dependency.is_optional }}
<a href="?is_optional={{ dependency.is_optional }}" class="is-black-link">{{ dependency.is_optional }}</a>
</td>
<td>
{{ dependency.is_resolved }}
<a href="?is_resolved={{ dependency.is_resolved }}" class="is-black-link">{{ dependency.is_resolved }}</a>
</td>
<td>
{% if dependency.for_package %}
Expand All @@ -56,6 +56,12 @@
{{ dependency.datasource_id }}
</td>
</tr>
{% empty %}
<tr>
<td colspan="42" class="has-text-centered p-3">
No Dependencies found. <a href="?">Clear search and filters</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
Expand Down
6 changes: 6 additions & 0 deletions scanpipe/templates/scanpipe/error_list.html
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@
<pre class="log wrap p-0" style="max-height: 150px;"><code>{{ error.traceback }}</code></pre>
</td>
</tr>
{% empty %}
<tr>
<td colspan="42" class="has-text-centered p-3">
No Errors found. <a href="?">Clear search and filters</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
Expand Down
6 changes: 3 additions & 3 deletions scanpipe/templates/scanpipe/includes/file_filter.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@
<div class="tabs is-toggle is-toggle-rounded is-centered">
<ul>
<li{% if file_filter == "all" %} class="is-active"{% endif %}>
<a href="?file-filter=all#resource-charts">
<a href="?file-filter=all#charts">
All Files <span class="tag is-link is-light is-rounded ml-1">{{ project.file_count|intcomma }}</span>
</a>
</li>
<li{% if file_filter == "in-a-package" %} class="is-active"{% endif %}>
<a href="?file-filter=in-a-package#resource-charts">
<a href="?file-filter=in-a-package#charts">
In a Package <span class="tag is-link is-light is-rounded ml-1">{{ project.file_in_package_count|intcomma }}</span>
</a>
</li>
<li{% if file_filter == "not-in-a-package" %} class="is-active"{% endif %}>
<a href="?file-filter=not-in-a-package#resource-charts">
<a href="?file-filter=not-in-a-package#charts">
NOT in a Package <span class="tag is-link is-light is-rounded ml-1">{{ project.file_not_in_package_count|intcomma }}</span>
</a>
</li>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
<div class="dropdown is-hoverable">
<div class="dropdown is-hoverable {% if is_right %}is-right{% endif %}">
<div class="dropdown-trigger">
<a aria-haspopup="true" aria-controls="{{ filter_form_field.id_for_label }}">
<a class="{% if filter.data %}has-text-link{% else %}is-grey-link{% endif %}" aria-haspopup="true" aria-controls="{{ filter.id_for_label }}">
<i class="fa-solid fa-filter"></i>
</a>
</div>
<div class="dropdown-menu" id="{{ filter_form_field.id_for_label }}" role="menu">
<div class="dropdown-menu" id="{{ filter.id_for_label }}" role="menu">
<div class="dropdown-content">
{% for value, label in filter_form_field.field.choices %}
<a href="?{{ filter_form_field.html_name }}={{ value }}" class="dropdown-item">
{{ label }}
</a>
{% endfor %}
{{ filter }}
</div>
</div>
</div>
8 changes: 4 additions & 4 deletions scanpipe/templates/scanpipe/includes/filter_sort.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<a href="?sort={% if "-" not in filter.data.sort %}-{% endif %}{{ field_name }}" class="is-black-link">
{{ label }}
<a href="?{{ column.sort_query }}" class="is-black-link">
{{ column.label }}
</a>
{% if field_name in filter.data.sort %}
<i class="fa-solid fa-sort-{% if "-" in filter.data.sort %}down{% else %}up{% endif %}"></i>
{% if column.is_sorted %}
<i class="fa-solid fa-sort-{% if column.sort_direction == "-" %}down{% else %}up{% endif %}"></i>
{% endif %}
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
<div class="dropdown is-hoverable">
<div class="dropdown-trigger">
<button class="button is-success is-small mr-1" aria-haspopup="true" aria-controls="dropdown-menu-action">
<i class="fa-solid fa-download mr-2"></i> Export
<span class="icon mr-1">
<i class="fa-solid fa-download"></i>
</span>
Export
</button>
</div>
<div class="dropdown-menu" id="dropdown-menu-action" role="menu">
Expand Down
25 changes: 13 additions & 12 deletions scanpipe/templates/scanpipe/includes/list_view_thead.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,20 @@
<tr>
{% for column in columns_data %}
<th id="column-{{ column.field_name }}" {% if column.css_class %}class="{{ column.css_class }}"{% elif column.field_name in filter.data.sort %}class="nowrap"{% endif %}>
{% if column.sort_query %}
<a class="is-black-link" href="?{{ column.sort_query }}">{{ column.label }}</a>
{% if column.field_name in filter.data.sort %}
<i class="fa-solid fa-sort-{% if "-" in filter.data.sort %}down{% else %}up{% endif %}"></i>
{% endif %}
{% else %}
{{ column.label }}
{% endif %}
{% if column.field_name == "status" %}
<div class="is-pulled-right">
{% include 'scanpipe/includes/filter_dropdown_choices_field.html' with filter_form_field=filter.form.status only %}
<div class="is-flex is-justify-content-space-between">
<div>
{% if column.sort_query %}
{% include 'scanpipe/includes/filter_sort.html' with column=column only %}
{% else %}
{{ column.label }}
{% endif %}
</div>
{% endif %}
{% if column.filter %}
<div class="ml-1">
{% include 'scanpipe/includes/filter_dropdown_choices_field.html' with filter=column.filter is_right=column.filter_is_right only %}
</div>
{% endif %}
</div>
</th>
{% endfor %}
</tr>
Expand Down
12 changes: 10 additions & 2 deletions scanpipe/templates/scanpipe/package_list.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,15 @@
{% endif %}
</td>
<td style="min-width: 300px; max-width: 400px;">
{{ package.declared_license_expression|linebreaksbr }}
<a href="?declared_license_expression={{ package.declared_license_expression }}" class="is-black-link">
{{ package.declared_license_expression }}
</a>
</td>
<td title="{{ package.copyright }}">
{{ package.copyright|truncatechars:150|linebreaksbr }}
</td>
<td>
{{ package.primary_language }}
<a href="?primary_language={{ package.primary_language }}" class="is-black-link">{{ package.primary_language }}</a>
</td>
<td class="is-clipped-list">
{% if package.resources %}
Expand All @@ -53,6 +55,12 @@
{% endif %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="42" class="has-text-centered p-3">
No Packages found. <a href="?">Clear search and filters</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
Expand Down
2 changes: 1 addition & 1 deletion scanpipe/templates/scanpipe/project_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@
</section>

{% if project.resource_count or project.package_count %}
<section id="charts" class="mx-5 pt-5" hx-get="{% url 'project_charts' project.slug %}" hx-trigger="load">
<section id="charts" class="mx-5 pt-5" hx-get="{% url 'project_charts' project.slug %}?file-filter={{ file_filter }}" hx-trigger="load">
<i class="fa-solid fa-spinner fa-pulse" aria-hidden="true"></i>
Loading the charts...
</section>
Expand Down
Loading