Skip to content
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

Open
wants to merge 12 commits into
base: main
Choose a base branch
from

Conversation

Rishi-source
Copy link

@Rishi-source Rishi-source commented Oct 19, 2024

Add pagination to VCIO & SCIO #1616 #1617 #1618

This change aligns Package Search functionality and Vulnerability Search, improving consistency across the application and enhancing user control over result display.

For Package Search:

Screenshot 2024-10-18 at 4 42 58 PM

For Vulnerability Search:

Screenshot 2024-10-18 at 4 43 39 PM

Signed-off-by: Rishi Garg [email protected]

@Rishi-source
Copy link
Author

Hi @pombredanne @TG1999 @keshav-space @Hritik14 , Can you please review this PR It has passed all workflow tests and was also working fine on my local server. Thank you

@pombredanne
Copy link
Member

@TG1999 ping .... @tdruez input welcomed too, as eventually we want to use the same approach across all projects.

vulnerabilities/templates/vulnerabilities.html Outdated Show resolved Hide resolved
vulnerabilities/templates/vulnerabilities.html Outdated Show resolved Hide resolved
vulnerabilities/views.py Outdated Show resolved Hide resolved
vulnerabilities/views.py Outdated Show resolved Hide resolved
vulnerabilities/views.py Outdated Show resolved Hide resolved
vulnerabilities/views.py Outdated Show resolved Hide resolved
vulnerabilities/templates/packages.html Outdated Show resolved Hide resolved
@Rishi-source Rishi-source force-pushed the Page-Updated branch 4 times, most recently from d8068aa to c28765a Compare October 29, 2024 18:44
@Rishi-source Rishi-source requested a review from tdruez October 29, 2024 18:50
@Rishi-source Rishi-source force-pushed the Page-Updated branch 2 times, most recently from e4a1a18 to 8d177c8 Compare November 2, 2024 05:37
@Rishi-source
Copy link
Author

@tdruez please see the changes now workflow tests are also successful .

@Rishi-source
Copy link
Author

@tdruez I would request you to please review the PR I have made changes In the code after you requested some changes after first review.

Copy link
Contributor

@tdruez tdruez left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the various comments.
Generally, this implementation seems too complex and not "reusable" enough at it requires to many moving parts.

@@ -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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unwanted changes

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

from vulnerabilities.models import ApiUser

from .models import *
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid * imports.

widget=forms.Select(
attrs={
"class": "select is-small",
"onchange": "handlePageSizeChange(this.value)",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid inline JavaScript.

Comment on lines 46 to 47
def clean_search(self):
return self.cleaned_data.get("search", "")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the purpose of this?

Comment on lines 49 to 61
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())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the need for this logic?

Comment on lines 73 to 80
def _search(self, query):
"""Execute package-specific search logic."""
return (
self.model.objects.search(query)
.with_vulnerability_counts()
.prefetch_related()
.order_by("package_url")
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not following the Django forms conventions.

Comment on lines 92 to 94
def _search(self, query):
"""Execute vulnerability-specific search logic."""
return self.model.objects.search(query=query).with_package_counts()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as above.

Comment on lines 4 to 5
<a href="?page={{ page_obj.previous_page_number }}&search={{ search|urlencode }}&page_size={{ page_obj.paginator.per_page }}"
class="pagination-previous">Previous</a>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if other filters are active, those are lost when using the pagination?


Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All the complexity of those templates should likely be handled in Python.

model = models.Package
template_name = "packages.html"
ordering = ["type", "namespace", "name", "version"]
class BaseSearchView(ListView):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we use the django_filters.views.FilterView instead of re-implementing it?

@Rishi-source
Copy link
Author

See the various comments. Generally, this implementation seems too complex and not "reusable" enough at it requires to many moving parts.

@tdruez what do you mean by "reusable" here. I would code the implementation again to make it more simpler and satisfy the required needs.

@tdruez
Copy link
Contributor

tdruez commented Nov 20, 2024

@tdruez what do you mean by "reusable" here. I would code the implementation again to make it more simpler and satisfy the required needs.

@pombredanne as eventually we want to use the same approach across all projects.

@Rishi-source Let's say I want to add this pagination on another Django view, could you document the few steps I need to do with this current implementation? This would help to figure out where we can improve the "reusability".

{"value": 100, "label": "100 per page"},
]

def get_paginate_by(self, queryset=None):
Copy link
Author

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.

except (ValueError, TypeError):
return self.paginate_by

def get_context_data(self, **kwargs):
Copy link
Author

Choose a reason for hiding this comment

The 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.


return context

def _get_page_range(self, paginator, current_page, window=2):
Copy link
Author

Choose a reason for hiding this comment

The 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.

@@ -36,6 +36,8 @@
from vulnerabilities.utils import get_severity_range
from vulnerablecode.settings import env

from .pagination_mixin import PaginatedListViewMixin
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Import the Mixin and the PaginatedListViewMixin class.

@@ -62,25 +64,21 @@ def get_purl_version_class(purl: models.Package):
return purl_version_class


class PackageSearch(ListView):
class PackageSearch(PaginatedListViewMixin, ListView):
Copy link
Author

Choose a reason for hiding this comment

The 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.

@Rishi-source
Copy link
Author

Hi @tdruez , I have altered code to improve the reusability for that I have made a file pagination_mixin.py which contains PaginatedListViewMixin class . This logic can convert and ListView into a paginated class and can improve the reusability as now you would have to just add this class to you views.

@Rishi-source Rishi-source requested a review from tdruez December 8, 2024 06:56
window.location.href = newUrl;
}
</script>
Copy link
Author

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there any risk of XSS there?

@tdruez
Copy link
Contributor

tdruez commented Dec 9, 2024

@Rishi-source Thanks but looking at multiple comments is hard to follow. Could you also provide a combined documentation of the few steps required to reuse this pagination in another app?

@Rishi-source
Copy link
Author

Rishi-source commented Dec 9, 2024

Pagination Implementation Documentation

Core Components of Code

  1. Pagination Mixin (in pagination_mixins.py):
  2. Pagination Template (in templates/includes/pagination.html):

Implementation Steps

  1. Copy the Mixin

    • Create pagination_mixins.py in your app directory
    • Copy the PaginatedListViewMixin class code into it
  2. Create the Template

    • Create pagination.html in your templates directory
    • Copy the pagination template code into it
  3. Use PaginatedListViewMixin in Your View

from django.views.generic import ListView
from .pagination_mixins import PaginatedListViewMixin # Make sure to import the mixin

class MyListView(PaginatedListViewMixin, ListView): # Please make sure to put "PaginatedListViewMixin" Above all the mixins in case if there are 2 or more mixins are already their as position  of mixin will matter here
    model = MyModel
    template_name = "my_template.html"
    
    def get_queryset(self):
        return super().get_queryset().order_by('id') 
  1. Include in Your Pagination Template
{% extends "base.html" %}

{% block content %}

    {# Like This #}
    {% if is_paginated %}
        {% include 'pagination.html' with page_obj=page_obj %}
    {% endif %}

{% endblock %}

Customization Options

  1. Change Page Sizes
class MyListView(PaginatedListViewMixin, ListView):
    PAGE_SIZE_CHOICES = [
        {"value": 10, "label": "10 per page"},
        {"value": 25, "label": "25 per page"},
        {"value": 50, "label": "50 per page"},
    ]
    paginate_by = 20  # Set the Default size
  1. Adjust Window Size
def get_context_data(self, **kwargs):
    # Change window variable to show more/fewer pages around current page
    context = super().get_context_data(**kwargs)
    context['page_range'] = self._get_page_range(
        context['paginator'], 
        context['page_obj'].number,
        window=3  # Show 3 pages on each side i.e. left and right   
    return context

Notes

  • JavaScript is embedded directly in the template via the onchange handler
  • All pagination state is managed through URL parameters
  • All Filter terms are preserved across pagination
  • The mixin automatically handles invalid page numbers and sizes

Common Issues and Solutions

  1. Missing Page Numbers

    • Check that page_range is being passed correctly in context
  2. URL Parameters Not Preserved

    • Check template URLs include all necessary parameters
    • Verify parameter names match in views and templates
  3. Page Size Not Working

    • Verify page_size parameter is being passed correctly
    • Check get_paginate_by implementation in mixin
  4. Styling Issues

    • Ensure Bulma CSS is included if using default styling
    • Or customize template classes to match your CSS framework

Remember that this implementation assumes:

  • You're using Django's class-based views
  • You want URL parameter-based pagination
  • You're handling search through GET parameters

@Rishi-source
Copy link
Author

@tdruez Here’s the documentation on implementing pagination using this approach. Please review and let me know if any changes are required.

@Rishi-source
Copy link
Author

Hi @tdruez and @pombredanne , I have implemented this pagination code in both VCIO and SCIO and it is working nice.It preserved all the filter and add a option for the user to select the items per page. I have already shared the screenshot for the VCIO pagination view here are the screenshot for SCIO pagination.

@tdruez you just have to follow the steps which I have provided in the Documentation for implementing this pagination.

Screenshot 2024-12-10 at 7 41 46 PM Screenshot 2024-12-10 at 7 42 17 PM

@Rishi-source
Copy link
Author

Hi @TG1999 @tdruez , I request you to please review this PR I have completed the code .

Copy link
Member

@pombredanne pombredanne left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks
Here is some feedback!
The PR title should be using imperative style:
Not Pagination Updated - Items per page option added

But rather:
"Improve pagination in web ui"
then add details in the body.

Could you also add tests? PaginatedListViewMixin is not trivial code

Also as @tdruez mentioned in late November, the template should have little to no logic and instead the logic should be in the Python code.

Get the number of items to paginate by from the request.
"""
try:
page_size = int(self.request.GET.get("page_size", self.paginate_by))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the standard approach to get a request arg?


def get_pagination_context(self, paginator, page_obj):
"""
Generate pagination-related context data, preserving filters.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Generate pagination-related context data, preserving filters.
Return a mapping of pagination-related context data, preserving filters.

query_params = self.request.GET.copy()
query_params.pop("page", None)

base_query_string = query_params.urlencode()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the right way to generate a URL query string? Is there a better way?

window.location.href = newUrl;
}
</script>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there any risk of XSS there?

@@ -0,0 +1,87 @@
class PaginatedListViewMixin:
"""
A mixin that adds pagination functionality to ListView-based views.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would call the file pagination.py instead

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants