Skip to content

Commit 1302bed

Browse files
committed
Pagination via Mixin - Resolving Reusability issue
Signed-off-by: Rishi Garg <rishigarg2503@gmail.com>
1 parent 76afc2b commit 1302bed

File tree

7 files changed

+181
-245
lines changed

7 files changed

+181
-245
lines changed

vulnerabilities/forms.py

Lines changed: 3 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# VulnerableCode is a trademark of nexB Inc.
44
# SPDX-License-Identifier: Apache-2.0
55
# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
6-
# See https://github.com/nexB/vulnerablecode for support or download.
6+
# See https://github.com/aboutcode-org/vulnerablecode for support or download.
77
# See https://aboutcode.org for more information about nexB OSS projects.
88
#
99

@@ -12,87 +12,26 @@
1212

1313
from vulnerabilities.models import ApiUser
1414

15-
from .models import *
1615

16+
class PackageSearchForm(forms.Form):
1717

18-
class PaginationForm(forms.Form):
19-
"""Form to handle page size selection across the application."""
20-
21-
PAGE_CHOICES = [
22-
("20", "20 per page"),
23-
("50", "50 per page"),
24-
("100", "100 per page"),
25-
]
26-
27-
page_size = forms.ChoiceField(
28-
choices=PAGE_CHOICES,
29-
initial="20",
30-
required=False,
31-
widget=forms.Select(
32-
attrs={
33-
"class": "select is-small",
34-
"onchange": "handlePageSizeChange(this.value)",
35-
"id": "page-size-select",
36-
}
37-
),
38-
)
39-
40-
41-
class BaseSearchForm(forms.Form):
42-
"""Base form for implementing search functionality."""
43-
44-
search = forms.CharField(required=True)
45-
46-
def clean_search(self):
47-
return self.cleaned_data.get("search", "")
48-
49-
def get_queryset(self, query=None):
50-
"""
51-
Get queryset with search/filter/ordering applied.
52-
Args:
53-
query (str, optional): Direct query for testing
54-
"""
55-
if query is not None:
56-
return self._search(query)
57-
58-
if not self.is_valid():
59-
return self.model.objects.none()
60-
61-
return self._search(self.clean_search())
62-
63-
64-
class PackageSearchForm(BaseSearchForm):
65-
model = Package
6618
search = forms.CharField(
6719
required=True,
6820
widget=forms.TextInput(
6921
attrs={"placeholder": "Package name, purl or purl fragment"},
7022
),
7123
)
7224

73-
def _search(self, query):
74-
"""Execute package-specific search logic."""
75-
return (
76-
self.model.objects.search(query)
77-
.with_vulnerability_counts()
78-
.prefetch_related()
79-
.order_by("package_url")
80-
)
8125

26+
class VulnerabilitySearchForm(forms.Form):
8227

83-
class VulnerabilitySearchForm(BaseSearchForm):
84-
model = Vulnerability
8528
search = forms.CharField(
8629
required=True,
8730
widget=forms.TextInput(
8831
attrs={"placeholder": "Vulnerability id or alias such as CVE or GHSA"}
8932
),
9033
)
9134

92-
def _search(self, query):
93-
"""Execute vulnerability-specific search logic."""
94-
return self.model.objects.search(query=query).with_package_counts()
95-
9635

9736
class ApiUserCreationForm(forms.ModelForm):
9837
"""
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
class PaginatedListViewMixin:
2+
paginate_by = 20
3+
max_page_size = 100
4+
5+
PAGE_SIZE_CHOICES = [
6+
{"value": 20, "label": "20 per page"},
7+
{"value": 50, "label": "50 per page"},
8+
{"value": 100, "label": "100 per page"},
9+
]
10+
11+
def get_paginate_by(self, queryset=None):
12+
try:
13+
page_size = int(self.request.GET.get("page_size", self.paginate_by))
14+
if page_size <= 0:
15+
return self.paginate_by
16+
return min(page_size, self.max_page_size)
17+
except (ValueError, TypeError):
18+
return self.paginate_by
19+
20+
def get_context_data(self, **kwargs):
21+
context = super().get_context_data(**kwargs)
22+
23+
current_page_size = self.get_paginate_by()
24+
total_count = context["paginator"].count
25+
26+
context.update(
27+
{
28+
"current_page_size": current_page_size,
29+
"page_size_choices": self.PAGE_SIZE_CHOICES,
30+
"total_count": total_count,
31+
"page_range": self._get_page_range(
32+
context["paginator"], context["page_obj"].number
33+
),
34+
"search": self.request.GET.get("search", ""),
35+
}
36+
)
37+
38+
return context
39+
40+
def _get_page_range(self, paginator, current_page, window=2):
41+
if paginator.num_pages <= 5:
42+
return range(1, paginator.num_pages + 1)
43+
44+
pages = [1]
45+
if current_page > 3:
46+
pages.append("...")
47+
48+
start_page = max(2, current_page - window)
49+
end_page = min(paginator.num_pages - 1, current_page + window)
50+
pages.extend(range(start_page, end_page + 1))
51+
52+
if current_page < paginator.num_pages - 2:
53+
pages.append("...")
54+
55+
if paginator.num_pages not in pages:
56+
pages.append(paginator.num_pages)
57+
58+
return pages
Lines changed: 69 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,77 @@
1-
{% if is_paginated %}
2-
<nav class="pagination is-centered" role="navigation" aria-label="pagination">
3-
{% if page_obj.has_previous %}
4-
<a href="?page={{ page_obj.previous_page_number }}&search={{ search|urlencode }}&page_size={{ page_obj.paginator.per_page }}"
5-
class="pagination-previous">Previous</a>
6-
{% else %}
7-
<span class="pagination-previous" disabled>Previous</span>
8-
{% endif %}
1+
<div class="pagination-controls mb-4">
2+
<div class="is-flex is-justify-content-center mb-3">
3+
<div class="select is-small {% if total_count < current_page_size %}is-disabled{% endif %}">
4+
<select onchange="handlePageSizeChange(this.value)"
5+
{% if total_count < current_page_size %}disabled="disabled"{% endif %}>
6+
{% for choice in page_size_choices %}
7+
<option value="{{ choice.value }}"
8+
{% if choice.value == current_page_size %}selected{% endif %}>
9+
{{ choice.label }}
10+
</option>
11+
{% endfor %}
12+
</select>
13+
</div>
14+
</div>
915

10-
{% if page_obj.has_next %}
11-
<a href="?page={{ page_obj.next_page_number }}&search={{ search|urlencode }}&page_size={{ page_obj.paginator.per_page }}"
12-
class="pagination-next">Next</a>
13-
{% else %}
14-
<span class="pagination-next" disabled>Next</span>
15-
{% endif %}
16+
{% if page_obj.paginator.num_pages > 1 %}
17+
<nav class="pagination is-centered" role="navigation" aria-label="pagination">
18+
{% if page_obj.has_previous %}
19+
<a href="?page={{ page_obj.previous_page_number }}&search={{ search|urlencode }}&page_size={{ current_page_size }}"
20+
class="pagination-previous">Previous</a>
21+
{% else %}
22+
<button class="pagination-previous" disabled>Previous</button>
23+
{% endif %}
1624

17-
<ul class="pagination-list">
18-
{% if page_obj.number > 1 %}
19-
<li>
20-
<a href="?page=1&search={{ search|urlencode }}&page_size={{ page_obj.paginator.per_page }}"
21-
class="pagination-link" aria-label="Page 1">1</a>
22-
</li>
23-
{% if page_obj.number > 4 %}
24-
<li><span class="pagination-ellipsis">&hellip;</span></li>
25-
{% endif %}
25+
{% if page_obj.has_next %}
26+
<a href="?page={{ page_obj.next_page_number }}&search={{ search|urlencode }}&page_size={{ current_page_size }}"
27+
class="pagination-next">Next</a>
28+
{% else %}
29+
<button class="pagination-next" disabled>Next</button>
2630
{% endif %}
2731

28-
{% for i in page_obj.paginator.page_range %}
29-
{% if i > 1 and i < page_obj.paginator.num_pages %}
30-
{% if i >= page_obj.number|add:"-3" and i <= page_obj.number|add:"3" %}
32+
<ul class="pagination-list">
33+
{% for page_num in page_range %}
34+
{% if page_num == '...' %}
35+
<li><span class="pagination-ellipsis">&hellip;</span></li>
36+
{% else %}
3137
<li>
32-
{% if page_obj.number == i %}
33-
<span class="pagination-link is-current" aria-current="page">{{ i }}</span>
34-
{% else %}
35-
<a href="?page={{ i }}&search={{ search|urlencode }}&page_size={{ page_obj.paginator.per_page }}"
36-
class="pagination-link" aria-label="Goto page {{ i }}">{{ i }}</a>
37-
{% endif %}
38+
<a href="?page={{ page_num }}&search={{ search|urlencode }}&page_size={{ current_page_size }}"
39+
class="pagination-link {% if page_num == page_obj.number %}is-current{% endif %}"
40+
aria-label="Go to page {{ page_num }}"
41+
{% if page_num == page_obj.number %}aria-current="page"{% endif %}>
42+
{{ page_num }}
43+
</a>
3844
</li>
3945
{% endif %}
40-
{% endif %}
41-
{% endfor %}
46+
{% endfor %}
47+
</ul>
48+
</nav>
49+
{% endif %}
50+
</div>
4251

43-
{% if page_obj.number < page_obj.paginator.num_pages %}
44-
{% if page_obj.number < page_obj.paginator.num_pages|add:"-3" %}
45-
<li><span class="pagination-ellipsis">&hellip;</span></li>
46-
{% endif %}
47-
<li>
48-
<a href="?page={{ page_obj.paginator.num_pages }}&search={{ search|urlencode }}&page_size={{ page_obj.paginator.per_page }}"
49-
class="pagination-link" aria-label="Goto page {{ page_obj.paginator.num_pages }}">
50-
{{ page_obj.paginator.num_pages }}
51-
</a>
52-
</li>
53-
{% endif %}
54-
</ul>
55-
</nav>
56-
{% endif %}
52+
<style>
53+
.select.is-disabled {
54+
opacity: 0.7;
55+
cursor: not-allowed;
56+
}
57+
.select.is-disabled select {
58+
cursor: not-allowed;
59+
}
60+
</style>
61+
62+
<script>
63+
function handlePageSizeChange(value) {
64+
const url = new URL(window.location.href);
65+
const params = new URLSearchParams(url.search);
66+
params.set('page_size', value);
67+
params.delete('page');
68+
69+
const search = params.get('search');
70+
if (search) {
71+
params.set('search', search);
72+
}
73+
74+
const newUrl = `${window.location.pathname}?${params.toString()}`;
75+
window.location.href = newUrl;
76+
}
77+
</script>

vulnerabilities/templates/packages.html

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
{% extends "base.html" %}
2-
{% load static %}
32
{% load humanize %}
43
{% load widget_tweaks %}
54

@@ -19,11 +18,6 @@
1918
<div>
2019
{{ page_obj.paginator.count|intcomma }} results
2120
</div>
22-
<div class="is-flex is-justify-content-center mb-2">
23-
<div class="select is-small">
24-
{{ pagination_form.page_size }}
25-
</div>
26-
</div>
2721
{% if is_paginated %}
2822
{% include 'includes/pagination.html' with page_obj=page_obj %}
2923
{% endif %}
@@ -64,27 +58,27 @@
6458
<tr>
6559
<td style="word-break: break-all;">
6660
<a
67-
href="{{ package.get_absolute_url }}?search={{ search }}"
68-
target="_self">{{ package.purl }}</a>
61+
href="{{ package.get_absolute_url }}?search={{ search }}"
62+
target="_self">{{ package.purl }}</a>
6963
</td>
7064
<td>{{ package.vulnerability_count }}</td>
7165
<td>{{ package.patched_vulnerability_count }}</td>
7266
</tr>
7367
{% empty %}
7468
<tr>
7569
<td colspan="3" style="word-break: break-all;">
76-
No Package found.
70+
No Package found.
7771
</td>
7872
</tr>
7973
{% endfor %}
8074
</tbody>
8175
</table>
8276
</div>
8377

84-
{% if is_paginated %}
85-
{% include 'includes/pagination.html' with page_obj=page_obj %}
86-
{% endif %}
78+
{% if is_paginated %}
79+
{% include 'includes/pagination.html' with page_obj=page_obj %}
80+
{% endif %}
81+
8782
</section>
8883
{% endif %}
89-
<script src="{% static 'js/pagination.js' %}"></script>
90-
{% endblock %}
84+
{% endblock %}

0 commit comments

Comments
 (0)