diff --git a/vulnerabilities/forms.py b/vulnerabilities/forms.py
index a00885637..1123187b6 100644
--- a/vulnerabilities/forms.py
+++ b/vulnerabilities/forms.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.
#
@@ -12,9 +12,57 @@
from vulnerabilities.models import ApiUser
+from .models import *
-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", "")
+
+ def get_queryset(self, query=None):
+ """
+ Get queryset with search/filter/ordering applied.
+ Args:
+ query (str, optional): Direct query for testing
+ """
+ if query is not None:
+ return self._search(query)
+
+ if not self.is_valid():
+ return self.model.objects.none()
+
+ return self._search(self.clean_search())
+
+
+class PackageSearchForm(BaseSearchForm):
+ model = Package
search = forms.CharField(
required=True,
widget=forms.TextInput(
@@ -22,9 +70,18 @@ class PackageSearchForm(forms.Form):
),
)
+ def _search(self, query):
+ """Execute package-specific search logic."""
+ return (
+ self.model.objects.search(query)
+ .with_vulnerability_counts()
+ .prefetch_related()
+ .order_by("package_url")
+ )
-class VulnerabilitySearchForm(forms.Form):
+class VulnerabilitySearchForm(BaseSearchForm):
+ model = Vulnerability
search = forms.CharField(
required=True,
widget=forms.TextInput(
@@ -32,6 +89,10 @@ class VulnerabilitySearchForm(forms.Form):
),
)
+ def _search(self, query):
+ """Execute vulnerability-specific search logic."""
+ return self.model.objects.search(query=query).with_package_counts()
+
class ApiUserCreationForm(forms.ModelForm):
"""
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..0739b38b6 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 %}
@@ -58,8 +64,8 @@
{{ package.purl }}
+ href="{{ package.get_absolute_url }}?search={{ search }}"
+ target="_self">{{ package.purl }}
|
{{ package.vulnerability_count }} |
{{ package.patched_vulnerability_count }} |
@@ -67,7 +73,7 @@
{% empty %}
- No Package found.
+ No Package found.
|
{% endfor %}
@@ -75,10 +81,10 @@
- {% 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 %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/vulnerabilities/templates/vulnerabilities.html b/vulnerabilities/templates/vulnerabilities.html
index 023d3f97f..850e34322 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,9 +46,9 @@
{% for vulnerability in page_obj %}
- {{ vulnerability.vulnerability_id }}
+ {{ vulnerability.vulnerability_id }}
|
@@ -63,7 +69,7 @@
{% empty %}
|
- No vulnerability found.
+ No vulnerability found.
|
{% endfor %}
@@ -71,11 +77,10 @@
-
- {% 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 %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/vulnerabilities/views.py b/vulnerabilities/views.py
index 51cdcd049..32b748d6c 100644
--- a/vulnerabilities/views.py
+++ b/vulnerabilities/views.py
@@ -29,14 +29,15 @@
from vulnerabilities import models
from vulnerabilities.forms import ApiUserCreationForm
from vulnerabilities.forms import PackageSearchForm
+from vulnerabilities.forms import PaginationForm
from vulnerabilities.forms import VulnerabilitySearchForm
-from vulnerabilities.models import VulnerabilityStatusType
from vulnerabilities.severity_systems import EPSS
from vulnerabilities.severity_systems import SCORING_SYSTEMS
from vulnerabilities.utils import get_severity_range
from vulnerablecode.settings import env
PAGE_SIZE = 20
+MAX_PAGE_SIZE = 100
def purl_sort_key(purl: models.Package):
@@ -62,51 +63,99 @@ 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):
+ """
+ Get and validate the requested page size.
+ Required 2 positional_argument get_paginate_by(positional_argument1, positional_argument2)
+ """
+ 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):
+ """Add pagination form to the template context."""
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
+
+class PackageSearch(BaseSearchView):
+ model = models.Package
+ template_name = "packages.html"
+ form_class = PackageSearchForm
+ ordering = ["type", "namespace", "name", "version"]
+
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 ""
- return (
- self.model.objects.search(query)
- .with_vulnerability_counts()
- .prefetch_related()
- .order_by("package_url")
+ """Get queryset from form's search method."""
+ if query is not None:
+ form = self.form_class()
+ return form.get_queryset(query=query)
+
+ if hasattr(self, "request"):
+ self.form = self.form_class(self.request.GET)
+ return self.form.get_queryset()
+
+ return self.model.objects.none()
+
+ def get_context_data(self, **kwargs):
+ """Extends the template context with search form and search query for Packages."""
+ context = super().get_context_data(**kwargs)
+ if not hasattr(self, "form"):
+ self.form = self.form_class()
+ context.update(
+ {
+ "package_search_form": self.form,
+ "search": getattr(self.request, "GET", {}).get("search"),
+ }
)
+ return context
-class VulnerabilitySearch(ListView):
+class VulnerabilitySearch(BaseSearchView):
model = models.Vulnerability
template_name = "vulnerabilities.html"
+ form_class = VulnerabilitySearchForm
ordering = ["vulnerability_id"]
- paginate_by = PAGE_SIZE
+
+ def get_queryset(self, query=None):
+ """Get queryset from form's search method."""
+ if query is not None:
+ form = self.form_class()
+ return form.get_queryset(query=query)
+
+ if hasattr(self, "request"):
+ self.form = self.form_class(self.request.GET)
+ return self.form.get_queryset()
+
+ return self.model.objects.none()
def get_context_data(self, **kwargs):
+ """Extends the template context with search form and search query for Vulnerability."""
context = super().get_context_data(**kwargs)
- request_query = self.request.GET
- context["vulnerability_search_form"] = VulnerabilitySearchForm(request_query)
- context["search"] = request_query.get("search")
+ if not hasattr(self, "form"):
+ self.form = self.form_class()
+ context.update(
+ {
+ "vulnerability_search_form": self.form,
+ "search": getattr(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
@@ -250,11 +299,15 @@ def get(self, request):
Token {auth_token}
-If you did NOT request this API key, you can either ignore this email or contact us at support@nexb.com and let us know in the forward that you did not request an API key.
+If you did NOT request this API key, you can either ignore
+this email or contact us at support@nexb.com and let us know in the forward
+that you did not request an API key.
The API root is at https://public.vulnerablecode.io/api
-To learn more about using the VulnerableCode.io API, please refer to the live API documentation at https://public.vulnerablecode.io/api/docs
-To learn about VulnerableCode, refer to the general documentation at https://vulnerablecode.readthedocs.io
+To learn more about using the VulnerableCode.io API,
+please refer to the live API documentation at https://public.vulnerablecode.io/api/docs
+To learn about VulnerableCode, refer to the
+general documentation at https://vulnerablecode.readthedocs.io
--
Sincerely,
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