Skip to content

Commit

Permalink
Pagination via Mixin - Resolving Reusability issue
Browse files Browse the repository at this point in the history
Signed-off-by: Rishi Garg <rishigarg2503@gmail.com>
  • Loading branch information
Rishi-garg03 committed Dec 8, 2024
1 parent 76afc2b commit 1302bed
Show file tree
Hide file tree
Showing 7 changed files with 181 additions and 245 deletions.
67 changes: 3 additions & 64 deletions vulnerabilities/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -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/nexB/vulnerablecode for support or download.
# See https://github.com/aboutcode-org/vulnerablecode for support or download.
# See https://aboutcode.org for more information about nexB OSS projects.
#

Expand All @@ -12,87 +12,26 @@

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(
attrs={"placeholder": "Package name, purl or purl fragment"},
),
)

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(
attrs={"placeholder": "Vulnerability id or alias such as CVE or GHSA"}
),
)

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


class ApiUserCreationForm(forms.ModelForm):
"""
Expand Down
58 changes: 58 additions & 0 deletions vulnerabilities/pagination_mixin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
class PaginatedListViewMixin:
paginate_by = 20
max_page_size = 100

PAGE_SIZE_CHOICES = [
{"value": 20, "label": "20 per page"},
{"value": 50, "label": "50 per page"},
{"value": 100, "label": "100 per page"},
]

def get_paginate_by(self, queryset=None):
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)

current_page_size = self.get_paginate_by()
total_count = context["paginator"].count

context.update(
{
"current_page_size": current_page_size,
"page_size_choices": self.PAGE_SIZE_CHOICES,
"total_count": total_count,
"page_range": self._get_page_range(
context["paginator"], context["page_obj"].number
),
"search": self.request.GET.get("search", ""),
}
)

return context

def _get_page_range(self, paginator, current_page, window=2):
if paginator.num_pages <= 5:
return range(1, paginator.num_pages + 1)

pages = [1]
if current_page > 3:
pages.append("...")

start_page = max(2, current_page - window)
end_page = min(paginator.num_pages - 1, current_page + window)
pages.extend(range(start_page, end_page + 1))

if current_page < paginator.num_pages - 2:
pages.append("...")

if paginator.num_pages not in pages:
pages.append(paginator.num_pages)

return pages
117 changes: 69 additions & 48 deletions vulnerabilities/templates/includes/pagination.html
Original file line number Diff line number Diff line change
@@ -1,56 +1,77 @@
{% if is_paginated %}
<nav class="pagination is-centered" role="navigation" aria-label="pagination">
{% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}&search={{ search|urlencode }}&page_size={{ page_obj.paginator.per_page }}"
class="pagination-previous">Previous</a>
{% else %}
<span class="pagination-previous" disabled>Previous</span>
{% endif %}
<div class="pagination-controls mb-4">
<div class="is-flex is-justify-content-center mb-3">
<div class="select is-small {% if total_count < current_page_size %}is-disabled{% endif %}">
<select onchange="handlePageSizeChange(this.value)"
{% if total_count < current_page_size %}disabled="disabled"{% endif %}>
{% for choice in page_size_choices %}
<option value="{{ choice.value }}"
{% if choice.value == current_page_size %}selected{% endif %}>
{{ choice.label }}
</option>
{% endfor %}
</select>
</div>
</div>

{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}&search={{ search|urlencode }}&page_size={{ page_obj.paginator.per_page }}"
class="pagination-next">Next</a>
{% else %}
<span class="pagination-next" disabled>Next</span>
{% endif %}
{% if page_obj.paginator.num_pages > 1 %}
<nav class="pagination is-centered" role="navigation" aria-label="pagination">
{% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}&search={{ search|urlencode }}&page_size={{ current_page_size }}"
class="pagination-previous">Previous</a>
{% else %}
<button class="pagination-previous" disabled>Previous</button>
{% endif %}

<ul class="pagination-list">
{% if page_obj.number > 1 %}
<li>
<a href="?page=1&search={{ search|urlencode }}&page_size={{ page_obj.paginator.per_page }}"
class="pagination-link" aria-label="Page 1">1</a>
</li>
{% if page_obj.number > 4 %}
<li><span class="pagination-ellipsis">&hellip;</span></li>
{% endif %}
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}&search={{ search|urlencode }}&page_size={{ current_page_size }}"
class="pagination-next">Next</a>
{% else %}
<button class="pagination-next" disabled>Next</button>
{% endif %}

{% for i in page_obj.paginator.page_range %}
{% if i > 1 and i < page_obj.paginator.num_pages %}
{% if i >= page_obj.number|add:"-3" and i <= page_obj.number|add:"3" %}
<ul class="pagination-list">
{% for page_num in page_range %}
{% if page_num == '...' %}
<li><span class="pagination-ellipsis">&hellip;</span></li>
{% else %}
<li>
{% if page_obj.number == i %}
<span class="pagination-link is-current" aria-current="page">{{ i }}</span>
{% else %}
<a href="?page={{ i }}&search={{ search|urlencode }}&page_size={{ page_obj.paginator.per_page }}"
class="pagination-link" aria-label="Goto page {{ i }}">{{ i }}</a>
{% endif %}
<a href="?page={{ page_num }}&search={{ search|urlencode }}&page_size={{ current_page_size }}"
class="pagination-link {% if page_num == page_obj.number %}is-current{% endif %}"
aria-label="Go to page {{ page_num }}"
{% if page_num == page_obj.number %}aria-current="page"{% endif %}>
{{ page_num }}
</a>
</li>
{% endif %}
{% endif %}
{% endfor %}
{% endfor %}
</ul>
</nav>
{% endif %}
</div>

{% if page_obj.number < page_obj.paginator.num_pages %}
{% if page_obj.number < page_obj.paginator.num_pages|add:"-3" %}
<li><span class="pagination-ellipsis">&hellip;</span></li>
{% endif %}
<li>
<a href="?page={{ page_obj.paginator.num_pages }}&search={{ search|urlencode }}&page_size={{ page_obj.paginator.per_page }}"
class="pagination-link" aria-label="Goto page {{ page_obj.paginator.num_pages }}">
{{ page_obj.paginator.num_pages }}
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
<style>
.select.is-disabled {
opacity: 0.7;
cursor: not-allowed;
}
.select.is-disabled select {
cursor: not-allowed;
}
</style>

<script>
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()}`;
window.location.href = newUrl;
}
</script>
22 changes: 8 additions & 14 deletions vulnerabilities/templates/packages.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
{% extends "base.html" %}
{% load static %}
{% load humanize %}
{% load widget_tweaks %}

Expand All @@ -19,11 +18,6 @@
<div>
{{ page_obj.paginator.count|intcomma }} results
</div>
<div class="is-flex is-justify-content-center mb-2">
<div class="select is-small">
{{ pagination_form.page_size }}
</div>
</div>
{% if is_paginated %}
{% include 'includes/pagination.html' with page_obj=page_obj %}
{% endif %}
Expand Down Expand Up @@ -64,27 +58,27 @@
<tr>
<td style="word-break: break-all;">
<a
href="{{ package.get_absolute_url }}?search={{ search }}"
target="_self">{{ package.purl }}</a>
href="{{ package.get_absolute_url }}?search={{ search }}"
target="_self">{{ package.purl }}</a>
</td>
<td>{{ package.vulnerability_count }}</td>
<td>{{ package.patched_vulnerability_count }}</td>
</tr>
{% empty %}
<tr>
<td colspan="3" style="word-break: break-all;">
No Package found.
No Package found.
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>

{% 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 %}

</section>
{% endif %}
<script src="{% static 'js/pagination.js' %}"></script>
{% endblock %}
{% endblock %}
Loading

0 comments on commit 1302bed

Please sign in to comment.