diff --git a/vulnerabilities/forms.py b/vulnerabilities/forms.py index a00885637..8f887cdca 100644 --- a/vulnerabilities/forms.py +++ b/vulnerabilities/forms.py @@ -3,18 +3,49 @@ # VulnerableCode is a trademark of nexB Inc. # SPDX-License-Identifier: Apache-2.0 # See http://www.apache.org/licenses/LICENSE-2.0 for the license text. -# See https://github.com/aboutcode-org/vulnerablecode for support or download. +# See https://github.com/nexB/vulnerablecode for support or download. # See https://aboutcode.org for more information about nexB OSS projects. # from django import forms from django.core.validators import validate_email - +from .models import * from vulnerabilities.models import ApiUser -class PackageSearchForm(forms.Form): +class PaginationForm(forms.Form): + """Form to handle page size selection across the application.""" + + PAGE_CHOICES = [ + ("20", "20 per page"), + ("50", "50 per page"), + ("100", "100 per page"), + ] + + page_size = forms.ChoiceField( + choices=PAGE_CHOICES, + initial="20", + required=False, + widget=forms.Select( + attrs={ + "class": "select is-small", + "onchange": "handlePageSizeChange(this.value)", + "id": "page-size-select", + } + ), + ) + +class BaseSearchForm(forms.Form): + """Base form for implementing search functionality.""" + + search = forms.CharField(required=True) + + def clean_search(self): + return self.cleaned_data.get("search", "") + + +class PackageSearchForm(BaseSearchForm): search = forms.CharField( required=True, widget=forms.TextInput( @@ -23,8 +54,7 @@ class PackageSearchForm(forms.Form): ) -class VulnerabilitySearchForm(forms.Form): - +class VulnerabilitySearchForm(BaseSearchForm): search = forms.CharField( required=True, widget=forms.TextInput( diff --git a/vulnerabilities/templates/includes/pagination.html b/vulnerabilities/templates/includes/pagination.html index 0d6dad430..b57e83850 100644 --- a/vulnerabilities/templates/includes/pagination.html +++ b/vulnerabilities/templates/includes/pagination.html @@ -1,39 +1,56 @@ - \ No newline at end of file + + + +{% endif %} \ No newline at end of file diff --git a/vulnerabilities/templates/packages.html b/vulnerabilities/templates/packages.html index 1f7687429..9e3e23eb2 100644 --- a/vulnerabilities/templates/packages.html +++ b/vulnerabilities/templates/packages.html @@ -1,4 +1,5 @@ {% extends "base.html" %} +{% load static %} {% load humanize %} {% load widget_tweaks %} @@ -18,6 +19,11 @@
{{ page_obj.paginator.count|intcomma }} results
+
+
+ {{ pagination_form.page_size }} +
+
{% if is_paginated %} {% include 'includes/pagination.html' with page_obj=page_obj %} {% endif %} @@ -57,9 +63,8 @@ {% for package in page_obj %} - {{ package.purl }} + {{ package.purl }} {{ package.vulnerability_count }} {{ package.patched_vulnerability_count }} @@ -67,7 +72,7 @@ {% empty %} - No Package found. + No Package found. {% endfor %} @@ -75,10 +80,11 @@ - {% if is_paginated %} - {% include 'includes/pagination.html' with page_obj=page_obj %} - {% endif %} - + {% if is_paginated %} + {% include 'includes/pagination.html' with page_obj=page_obj %} + {% endif %} {% endif %} + {% endblock %} + diff --git a/vulnerabilities/templates/vulnerabilities.html b/vulnerabilities/templates/vulnerabilities.html index 023d3f97f..d7d2484aa 100644 --- a/vulnerabilities/templates/vulnerabilities.html +++ b/vulnerabilities/templates/vulnerabilities.html @@ -1,4 +1,5 @@ {% extends "base.html" %} +{% load static %} {% load humanize %} {% load widget_tweaks %} @@ -18,9 +19,14 @@
{{ page_obj.paginator.count|intcomma }} results
- {% if is_paginated %} - {% include 'includes/pagination.html' with page_obj=page_obj %} - {% endif %} +
+
+ {{ pagination_form.page_size }} +
+
+ {% if is_paginated %} + {% include 'includes/pagination.html' with page_obj=page_obj %} + {% endif %} @@ -40,10 +46,8 @@ {% for vulnerability in page_obj %} - {{ vulnerability.vulnerability_id }} - + {{ vulnerability.vulnerability_id }} {% for alias in vulnerability.alias %} @@ -62,8 +66,8 @@ {% empty %} - - No vulnerability found. + + No vulnerability found. {% endfor %} @@ -71,11 +75,11 @@ - - {% if is_paginated %} - {% include 'includes/pagination.html' with page_obj=page_obj %} - {% endif %} + {% if is_paginated %} + {% include 'includes/pagination.html' with page_obj=page_obj %} + {% endif %} {% endif %} - + {% endblock %} + diff --git a/vulnerabilities/views.py b/vulnerabilities/views.py index 51cdcd049..9b1f81d06 100644 --- a/vulnerabilities/views.py +++ b/vulnerabilities/views.py @@ -3,7 +3,7 @@ # VulnerableCode is a trademark of nexB Inc. # SPDX-License-Identifier: Apache-2.0 # See http://www.apache.org/licenses/LICENSE-2.0 for the license text. -# See https://github.com/aboutcode-org/vulnerablecode for support or download. +# See https://github.com/nexB/vulnerablecode for support or download. # See https://aboutcode.org for more information about nexB OSS projects. # import logging @@ -25,11 +25,12 @@ from django.views.generic.list import ListView from univers.version_range import RANGE_CLASS_BY_SCHEMES from univers.version_range import AlpineLinuxVersionRange - +from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from vulnerabilities import models from vulnerabilities.forms import ApiUserCreationForm from vulnerabilities.forms import PackageSearchForm from vulnerabilities.forms import VulnerabilitySearchForm +from vulnerabilities.forms import PaginationForm from vulnerabilities.models import VulnerabilityStatusType from vulnerabilities.severity_systems import EPSS from vulnerabilities.severity_systems import SCORING_SYSTEMS @@ -37,7 +38,7 @@ from vulnerablecode.settings import env PAGE_SIZE = 20 - +MAX_PAGE_SIZE = 100 def purl_sort_key(purl: models.Package): """ @@ -62,52 +63,84 @@ def get_purl_version_class(purl: models.Package): return purl_version_class -class PackageSearch(ListView): - model = models.Package - template_name = "packages.html" - ordering = ["type", "namespace", "name", "version"] +class BaseSearchView(ListView): + """Base view for implementing search functionality with pagination.""" + paginate_by = PAGE_SIZE + max_page_size = MAX_PAGE_SIZE + + def get_paginate_by(self, queryset=None): + """ + This function would get and validate the page size from request parameters. + It returns a page size between 1 and max_page_size, defaulting to + self.paginate_by for invalid inputs. + """ + try: + page_size = int(self.request.GET.get('page_size', self.paginate_by)) + if page_size <= 0: + return self.paginate_by + return min(page_size, self.max_page_size) + except (ValueError, TypeError): + return self.paginate_by def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - request_query = self.request.GET - context["package_search_form"] = PackageSearchForm(request_query) - context["search"] = request_query.get("search") + context.update({ + 'pagination_form': PaginationForm( + initial={'page_size': self.get_paginate_by()} + ), + }) return context - def get_queryset(self, query=None): - """ - Return a Package queryset for the ``query``. - Make a best effort approach to find matching packages either based - on exact purl, partial purl or just name and namespace. - """ - query = query or self.request.GET.get("search") or "" + +class PackageSearch(BaseSearchView): + model = models.Package + template_name = "packages.html" + ordering = ["type", "namespace", "name", "version"] + + def get_queryset(self): + """Return search results from the form.""" + self.form = PackageSearchForm(self.request.GET) + if not self.form.is_valid(): + return self.model.objects.none() return ( - self.model.objects.search(query) + self.model.objects.search(self.form.cleaned_data.get("search", "")) .with_vulnerability_counts() .prefetch_related() .order_by("package_url") ) + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context.update({ + "package_search_form": self.form, + "search": self.request.GET.get("search"), + }) + return context + -class VulnerabilitySearch(ListView): +class VulnerabilitySearch(BaseSearchView): model = models.Vulnerability template_name = "vulnerabilities.html" ordering = ["vulnerability_id"] - paginate_by = PAGE_SIZE + + def get_queryset(self): + """Return search results from the form.""" + self.form = VulnerabilitySearchForm(self.request.GET) + if not self.form.is_valid(): + return self.model.objects.none() + return ( + self.model.objects.search(self.form.cleaned_data.get("search", "")) + .with_package_counts() + ) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - request_query = self.request.GET - context["vulnerability_search_form"] = VulnerabilitySearchForm(request_query) - context["search"] = request_query.get("search") + context.update({ + "vulnerability_search_form": self.form, + "search": self.request.GET.get("search"), + }) return context - - def get_queryset(self, query=None): - query = query or self.request.GET.get("search") or "" - return self.model.objects.search(query=query).with_package_counts() - - class PackageDetails(DetailView): model = models.Package template_name = "package_details.html" diff --git a/vulnerablecode/static/js/pagination.js b/vulnerablecode/static/js/pagination.js new file mode 100644 index 000000000..c09bd10f0 --- /dev/null +++ b/vulnerablecode/static/js/pagination.js @@ -0,0 +1,17 @@ +// static/js/pagination.js +// This function would handles the pagination dropdown change event, maintaining existing search parameters. +// This would also update the page size in the URL and reloads the page with the new page size parameter. +function handlePageSizeChange(value) { + const url = new URL(window.location.href); + const params = new URLSearchParams(url.search); + params.set('page_size', value); + params.delete('page'); + const search = params.get('search'); + if (search) { + params.set('search', search); + } + const newUrl = `${window.location.pathname}?${params.toString()}`; + if (window.location.href !== newUrl) { + window.location.href = newUrl; + } +} \ No newline at end of file