-
Notifications
You must be signed in to change notification settings - Fork 202
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Pagination Updated - Items per page option added #1625
base: main
Are you sure you want to change the base?
Changes from 7 commits
6ca25ee
c28dd61
74ec708
397210d
29ff24c
76afc2b
1302bed
2e33c5b
3b21946
5752e23
7b1de51
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It adds pagination data to template context - current page size, size choices (can be handled by altering PAGE_SIZE_CHOICES) , total count and page range. it also preserves search params across pagination. |
||
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): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it creates page number sequence. Shows all numbers for small sets (<5 pages). For larger sets, shows first/last pages around current page with (...) for gaps. |
||
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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,39 +1,77 @@ | ||
<nav class="pagination is-centered is-small" aria-label="pagination"> | ||
{% if page_obj.has_previous %} | ||
<a href="?page={{ page_obj.previous_page_number }}&search={{ search|urlencode }}" class="pagination-previous">Previous</a> | ||
{% else %} | ||
<a class="pagination-previous" disabled>Previous</a> | ||
{% 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 }}" class="pagination-next">Next</a> | ||
{% else %} | ||
<a class="pagination-next" disabled>Next</a> | ||
{% 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 %} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All the complexity of those templates should likely be handled in Python. |
||
<ul class="pagination-list"> | ||
{% if page_obj.number != 1%} | ||
<li> | ||
<a href="?page=1&search={{ search|urlencode }}" class="pagination-link" aria-label="Goto page 1">1</a> | ||
</li> | ||
{% if page_obj.number > 2 %} | ||
<li> | ||
<span class="pagination-ellipsis">…</span> | ||
</li> | ||
{% endif %} | ||
{% endif %} | ||
<li> | ||
<a class="pagination-link is-current" aria-label="Page {{ page_obj.number }}" aria-current="page">{{ page_obj.number }}</a> | ||
</li> | ||
{% if page_obj.number != page_obj.paginator.num_pages %} | ||
{% if page_obj.next_page_number != page_obj.paginator.num_pages %} | ||
<li> | ||
<span class="pagination-ellipsis">…</span> | ||
</li> | ||
{% endif %} | ||
<li> | ||
<a href="?page={{ page_obj.paginator.num_pages }}&search={{ search|urlencode }}" class="pagination-link" aria-label="Goto page {{ page_obj.paginator.num_pages }}">{{ page_obj.paginator.num_pages }}</a> | ||
</li> | ||
{% 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 %} | ||
|
||
<ul class="pagination-list"> | ||
{% for page_num in page_range %} | ||
{% if page_num == '...' %} | ||
<li><span class="pagination-ellipsis">…</span></li> | ||
{% else %} | ||
<li> | ||
<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 %} | ||
{% endfor %} | ||
</ul> | ||
</nav> | ||
{% endif %} | ||
</ul> | ||
</nav> | ||
</div> | ||
|
||
<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> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This js function manages URL parameters when changing pagination size. It update the page size parameter while preserving search terms, and remove the current page parameter to reset pagination. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -36,6 +36,8 @@ | |
from vulnerabilities.utils import get_severity_range | ||
from vulnerablecode.settings import env | ||
|
||
from .pagination_mixin import PaginatedListViewMixin | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Import the Mixin and the PaginatedListViewMixin class. |
||
|
||
PAGE_SIZE = 20 | ||
|
||
|
||
|
@@ -62,25 +64,21 @@ def get_purl_version_class(purl: models.Package): | |
return purl_version_class | ||
|
||
|
||
class PackageSearch(ListView): | ||
class PackageSearch(PaginatedListViewMixin, ListView): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add the PaginatedListViewMixin in it and this would convert any ListView into paginated class. |
||
""" | ||
View for searching and displaying packages with pagination. | ||
""" | ||
|
||
model = models.Package | ||
template_name = "packages.html" | ||
ordering = ["type", "namespace", "name", "version"] | ||
paginate_by = PAGE_SIZE | ||
|
||
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["package_search_form"] = PackageSearchForm(self.request.GET) | ||
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 "" | ||
return ( | ||
self.model.objects.search(query) | ||
|
@@ -90,17 +88,18 @@ def get_queryset(self, query=None): | |
) | ||
|
||
|
||
class VulnerabilitySearch(ListView): | ||
class VulnerabilitySearch(PaginatedListViewMixin, ListView): | ||
""" | ||
View for searching and displaying vulnerabilities with pagination. | ||
""" | ||
|
||
model = models.Vulnerability | ||
template_name = "vulnerabilities.html" | ||
ordering = ["vulnerability_id"] | ||
paginate_by = PAGE_SIZE | ||
|
||
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["vulnerability_search_form"] = VulnerabilitySearchForm(self.request.GET) | ||
return context | ||
|
||
def get_queryset(self, query=None): | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function handles page size from URL parameters. Sets default to 20 items, max of 100. Validates to ensure positive integers only. Returns default if invalid values provided.