Skip to content

Commit fb02397

Browse files
authored
Refine Project list filters #413 (#465)
Signed-off-by: Thomas Druez <[email protected]>
1 parent 2deeb8c commit fb02397

File tree

10 files changed

+120
-57
lines changed

10 files changed

+120
-57
lines changed

CHANGELOG.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,15 @@ v31.0.0 (next)
4747
details view. The summary is generated during the `scan_package` pipeline.
4848
https://github.com/nexB/scancode.io/issues/411
4949

50+
- Enhance Project list view page:
51+
52+
- 20 projects are now displayed per page
53+
- Creation date displayed under the project name
54+
- Add ability to sort by date and name
55+
- Add ability to filter by pipeline type
56+
57+
https://github.com/nexB/scancode.io/issues/413
58+
5059
v30.2.0 (2021-12-17)
5160
--------------------
5261

scanpipe/filters.py

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,21 @@
2020
# ScanCode.io is a free software code scanning tool from nexB Inc. and others.
2121
# Visit https://github.com/nexB/scancode.io for support and download.
2222

23+
from django.apps import apps
2324
from django.db import models
2425
from django.utils.translation import gettext_lazy as _
2526

2627
import django_filters
28+
from django_filters.widgets import LinkWidget
2729
from packageurl.contrib.django.filters import PackageURLFilter
2830

2931
from scanpipe.models import CodebaseResource
3032
from scanpipe.models import DiscoveredPackage
3133
from scanpipe.models import Project
3234
from scanpipe.models import ProjectError
3335

36+
scanpipe_app = apps.get_app_config("scanpipe")
37+
3438

3539
class FilterSetUtilsMixin:
3640
@staticmethod
@@ -86,25 +90,75 @@ def verbose_name_plural(cls):
8690
return cls.Meta.model._meta.verbose_name_plural
8791

8892

93+
class BulmaLinkWidget(LinkWidget):
94+
"""
95+
Replace LinkWidget rendering with Bulma CSS classes.
96+
"""
97+
98+
extra_css_class = ""
99+
100+
def render_option(self, name, selected_choices, option_value, option_label):
101+
option = super().render_option(
102+
name, selected_choices, option_value, option_label
103+
)
104+
css_class = str(self.extra_css_class)
105+
106+
selected_class = ' class="selected"'
107+
if selected_class in option:
108+
option = option.replace(selected_class, "")
109+
css_class += " is-active"
110+
111+
option = option.replace("<a", f'<a class="{css_class}"')
112+
return option
113+
114+
115+
class BulmaDropdownWidget(BulmaLinkWidget):
116+
extra_css_class = "dropdown-item"
117+
118+
89119
class ProjectFilterSet(FilterSetUtilsMixin, django_filters.FilterSet):
90120
search = django_filters.CharFilter(field_name="name", lookup_expr="icontains")
91121
sort = django_filters.OrderingFilter(
92-
fields=(("name", "name"), ("created_date", "created_date")),
122+
label=_("Sort"),
123+
fields=["created_date", "name"],
124+
empty_label="Newest",
125+
choices=(
126+
("created_date", "Oldest"),
127+
("name", "Name (a-Z)"),
128+
("-name", "Name (Z-a)"),
129+
),
130+
widget=BulmaDropdownWidget,
131+
)
132+
pipeline = django_filters.ChoiceFilter(
133+
label=_("Pipeline"),
134+
field_name="runs__pipeline_name",
135+
choices=scanpipe_app.get_pipeline_choices(include_blank=False),
136+
widget=BulmaDropdownWidget,
93137
)
94138

95139
class Meta:
96140
model = Project
97-
fields = ["search", "is_archived"]
141+
fields = ["is_archived"]
98142

99143
def __init__(self, data=None, *args, **kwargs):
100144
"""
101145
Filter out the archived projects by default.
102146
"""
103147
super().__init__(data, *args, **kwargs)
104148

105-
if not data or "is_archived" not in data:
149+
# Default filtering by "Active" projects.
150+
if not data or data.get("is_archived", "") == "":
106151
self.queryset = self.queryset.filter(is_archived=False)
107152

153+
active_count = Project.objects.filter(is_archived=False).count()
154+
archived_count = Project.objects.filter(is_archived=True).count()
155+
self.filters["is_archived"].extra["widget"] = BulmaLinkWidget(
156+
choices=[
157+
("", f'<i class="fas fa-seedling"></i> {active_count} Active'),
158+
("true", f'<i class="fas fa-dice-d6"></i> {archived_count} Archived'),
159+
]
160+
)
161+
108162

109163
class JSONContainsFilter(django_filters.CharFilter):
110164
"""

scanpipe/management/commands/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,12 @@ def get_run_status_code(self, run):
7171
return status.upper()
7272

7373
def display_status(self, project, verbosity):
74+
project_label = f"Project: {project.name}"
75+
if project.is_archived:
76+
project_label += " [archived]"
77+
7478
message = [
75-
self.style.HTTP_INFO(f"Project: {project.name}"),
79+
self.style.HTTP_INFO(project_label),
7680
]
7781

7882
if verbosity >= 2:

scanpipe/management/commands/list-project.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ def add_arguments(self, parser):
3939
parser.add_argument(
4040
"--include-archived",
4141
action="store_true",
42+
dest="include_archived",
4243
help="Include archived projects.",
4344
)
4445

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<div class="dropdown is-right is-hoverable">
2+
<div class="dropdown-trigger">
3+
<button class="button" aria-haspopup="true" aria-controls="dropdown-menu">
4+
<span>{{ filter_form_field.label }}</span>
5+
<span class="icon is-small">
6+
<i class="fas fa-angle-down" aria-hidden="true"></i>
7+
</span>
8+
</button>
9+
</div>
10+
<div class="dropdown-menu" id="dropdown-sort-filters" role="menu">
11+
<div class="dropdown-content">
12+
{{ filter_form_field }}
13+
</div>
14+
</div>
15+
</div>

scanpipe/templates/scanpipe/includes/project_list_table.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,4 @@
6666
</tr>
6767
{% endfor %}
6868
</tbody>
69-
</table>
69+
</table>
Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
<div class="field">
2-
<form method="get">
3-
<p class="control has-icons-left">
4-
<input class="input {{ extra_class }}" type="text" placeholder="Search {{ filter.verbose_name_plural }}" name="{{ filter.form.search.name }}" value="{{ filter.form.search.value|default_if_none:'' }}">
5-
<span class="icon is-small is-left">
6-
<i class="fas fa-search"></i>
7-
</span>
8-
</p>
9-
</form>
10-
</div>
1+
<form method="get">
2+
<p class="control has-icons-left">
3+
<input class="input {{ extra_class }}" type="text" placeholder="Search {{ filter.verbose_name_plural }}" name="{{ filter.form.search.name }}" value="{{ filter.form.search.value|default_if_none:'' }}">
4+
<span class="icon is-small is-left">
5+
<i class="fas fa-search"></i>
6+
</span>
7+
</p>
8+
</form>
Lines changed: 21 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
{% extends "scanpipe/base.html" %}
22
{% load humanize %}
33

4+
{% block extrahead %}
5+
<style>
6+
ul#id_is_archived {display: inline-flex;}
7+
ul#id_is_archived li {margin-right: 1rem;}
8+
ul#id_is_archived li a {color: #7a7a7a;}
9+
ul#id_is_archived li a:hover {text-decoration: underline;}
10+
ul#id_is_archived li a.is-active {color: #363636;}
11+
</style>
12+
{% endblock %}
13+
414
{% block content %}
515
<div class="container is-max-desktop">
616
{% include 'scanpipe/includes/navbar_header.html' %}
@@ -10,42 +20,21 @@
1020
<div class="is-flex is-justify-content-space-between mb-2">
1121
<div>
1222
{% include 'scanpipe/includes/breadcrumb.html' %}
13-
{% if archived_count > 0 %}
14-
<a href="{% url 'project_list' %}" class="{% if 'is_archived' in request.GET %}is-grey-link{% else %}is-black-link{% endif %}">
15-
<i class="fas fa-seedling"></i>
16-
{{ active_count }} Active
17-
</a>
18-
<a href="{% url 'project_list' %}?is_archived=true" class="ml-4 {% if 'is_archived' in request.GET %}is-black-link{% else %}is-grey-link{% endif %}">
19-
<i class="fas fa-dice-d6"></i>
20-
{{ archived_count }} Archived
21-
</a>
22-
{% endif %}
23+
{{ filter.form.is_archived }}
24+
</div>
25+
<a href="{% url 'project_add' %}" class="button is-link">New Project</a>
26+
</div>
27+
28+
<div class="is-flex mb-3">
29+
<div class="is-flex-grow-1 mr-2">
30+
{% include 'scanpipe/includes/search_field.html' %}
2331
</div>
2432
<div>
25-
<div class="dropdown is-right is-hoverable">
26-
<div class="dropdown-trigger">
27-
<button class="button" aria-haspopup="true" aria-controls="dropdown-menu">
28-
<span>Sort</span>
29-
<span class="icon is-small">
30-
<i class="fas fa-angle-down" aria-hidden="true"></i>
31-
</span>
32-
</button>
33-
</div>
34-
<div class="dropdown-menu" id="dropdown-sort-filters" role="menu">
35-
<div class="dropdown-content">
36-
<a href="?sort=name" class="dropdown-item">Name (A-Z)</a>
37-
<a href="?sort=-name" class="dropdown-item">Name (Z-A)</a>
38-
<a href="?sort=created_date" class="dropdown-item">Oldest</a>
39-
<a href="?sort=-created_date" class="dropdown-item">Newest</a>
40-
</div>
41-
</div>
42-
</div>
43-
<a href="{% url 'project_add' %}" class="button is-link">New Project</a>
33+
{% include 'scanpipe/includes/filter_dropdown.html' with filter_form_field=filter.form.pipeline only %}
34+
{% include 'scanpipe/includes/filter_dropdown.html' with filter_form_field=filter.form.sort only %}
4435
</div>
4536
</div>
4637

47-
{% include 'scanpipe/includes/search_field.html' %}
48-
4938
{% if object_list %}
5039
{% include 'scanpipe/includes/project_list_table.html' with projects=object_list only %}
5140
{% else %}
@@ -69,4 +58,4 @@
6958
</div>
7059

7160
{% include 'scanpipe/includes/run_modal.html' %}
72-
{% endblock %}
61+
{% endblock %}

scanpipe/tests/test_views.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,15 +47,15 @@ def setUp(self):
4747
def test_scanpipe_views_project_list_is_archived(self):
4848
project2 = Project.objects.create(name="project2", is_archived=True)
4949
url = reverse("project_list")
50-
url_with_filter = url + "?is_archived=true"
50+
is_archive_filter = "?is_archived=true"
5151

5252
response = self.client.get(url)
5353
self.assertContains(response, self.project1.name)
5454
self.assertNotContains(response, project2.name)
5555
self.assertContains(response, url)
56-
self.assertContains(response, url_with_filter)
56+
self.assertContains(response, is_archive_filter)
5757

58-
response = self.client.get(url_with_filter)
58+
response = self.client.get(url + is_archive_filter)
5959
self.assertNotContains(response, self.project1.name)
6060
self.assertContains(response, project2.name)
6161

scanpipe/views.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@
3434
from django.shortcuts import redirect
3535
from django.shortcuts import render
3636
from django.urls import reverse_lazy
37-
from django.utils.html import escape
3837
from django.views import generic
3938
from django.views.generic.detail import SingleObjectMixin
4039
from django.views.generic.edit import FormView
@@ -187,12 +186,6 @@ class ProjectListView(
187186
prefetch_related = ["runs"]
188187
paginate_by = 20
189188

190-
def get_context_data(self, **kwargs):
191-
context = super().get_context_data(**kwargs)
192-
context["active_count"] = Project.objects.filter(is_archived=False).count()
193-
context["archived_count"] = Project.objects.filter(is_archived=True).count()
194-
return context
195-
196189

197190
class ProjectCreateView(ConditionalLoginRequired, generic.CreateView):
198191
model = Project

0 commit comments

Comments
 (0)