Skip to content

Fixed #36175 -- Refactor admin pagination to make it reusable across different queryset. #19172

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

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

Conversation

Antoliny0919
Copy link
Contributor

@Antoliny0919 Antoliny0919 commented Feb 13, 2025

Trac ticket number

ticket-36175

Branch description

Provide a concise overview of the issue or rationale behind the proposed changes.
Refactored the pagination in the admin interface by separating the parts that were specifically designed for ChangeList, making the Pagination class reusable across different queryset.

Checklist

  • This PR targets the main branch.
  • The commit message is written in past tense, mentions the ticket number, and ends with a period.
  • I have checked the "Has patch" ticket flag in the Trac system.
  • I have added or updated relevant tests.
  • I have added or updated relevant docs, including release notes if applicable.
  • I have attached screenshots in both light and dark modes for any UI changes.

@Antoliny0919 Antoliny0919 force-pushed the ticket_36175 branch 3 times, most recently from 53a20fe to 69216c0 Compare February 16, 2025 10:01
Copy link
Contributor

@nanuxbe nanuxbe left a comment

Choose a reason for hiding this comment

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

This looks like a good start.
My biggest concerns are about the missing deprecation if we are going with a changed API.
In order to validate that this is not breaking anything I would also duplicate the tests instead of changing them:

  • keep a copy of the old tests as they are + add a check that a deprecation warning is raised and a note to remove those after the deprecation cycle
  • add the changed version of the tests to validate the new API.

@@ -843,6 +843,14 @@ def get_changelist(self, request, **kwargs):

return ChangeList

def get_pagination(self, request, **kwargs):
Copy link
Contributor

Choose a reason for hiding this comment

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

Given this returns a class, is there a reason not to use get_pagination_class instead of get_pagination?
See also comment below in the same area

page_obj = paginator.get_page(page_number)
page_range = paginator.get_elided_page_range(page_obj.number)
Pagination = self.get_pagination(request)
pagination = Pagination(
Copy link
Contributor

Choose a reason for hiding this comment

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

I feel like having both Pagination and pagination can be quite confusing, especially since we have get_pagination that returns the class and not the instance.

This would look better to me if we had:

  • pagination_class and get_pagination_class instead of Pagination and get_pagination
  • maybe also have a new get_pagination that would be in charge of calling get_pagination_class and instantiating the pagination class instead of directly calling the Pagination class.

This would be similar to a pattern we have in other places in Django, like in the FormView https://ccbv.co.uk/projects/Django/5.0/django.views.generic.edit/FormView/#get_form

@@ -35,36 +36,37 @@


@register.simple_tag
def paginator_number(cl, i):
def paginator_number(p, i):
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm aware you are replacing the cl abbreviation that is 2 letters with a similar-type abbreviation for pagination but p is probably too short. Could this be pagination instead of just p?

Copy link
Member

Choose a reason for hiding this comment

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

Agree, better to have descriptive variable names to make the code more readable and self-documenting.

i,
)


def pagination(cl):
def pagination(p):
Copy link
Contributor

Choose a reason for hiding this comment

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

Same as above, one-letter variable names outside of very specific use-cases isn't great

@@ -82,6 +84,23 @@ def pagination_tag(parser, token):
)


def changelist_pagination(p, cl):
Copy link
Contributor

Choose a reason for hiding this comment

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

one-letter variable-name here as well

new_params = {}
if remove is None:
remove = []
p = params.copy()
Copy link
Contributor

Choose a reason for hiding this comment

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

Still not found of the one-letter variable names ;-)
p, k, r...

Even though I know you are just moving pre-existing code; if this piece of code is being touched, it's probably a good time to also make it more readable

def get_results(self, request):
paginator = self.model_admin.get_paginator(
request, self.queryset, self.list_per_page
pagination = Pagination(
Copy link
Contributor

Choose a reason for hiding this comment

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

Calling Pagination directly seems less flexible than having a separate get_paginator method.

If feels like the recommended approach from above (get_pagination_class + get_pagination would be more flexible and extendable)

self.show_full_result_count = self.model_admin.show_full_result_count
# Admin actions are shown if there is at least one entry
# or if entries are not counted because show_full_result_count is disabled
self.show_admin_actions = not self.show_full_result_count or bool(
full_result_count
)
self.full_result_count = full_result_count
self.result_list = result_list
self.can_show_all = can_show_all
Copy link
Contributor

Choose a reason for hiding this comment

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

The API of get_results is being changed here.
If this is the final approach, I don't see an issue with the change itself but we need to deprecate this properly:
ie:

  • deprecation warning
  • a workaround to populate the old properties while the deprecation is active

@Antoliny0919
Copy link
Contributor Author

This looks like a good start. My biggest concerns are about the missing deprecation if we are going with a changed API. In order to validate that this is not breaking anything I would also duplicate the tests instead of changing them:

* keep a copy of the old tests as they are + add a check that a deprecation warning is raised and a note to remove those after the deprecation cycle

* add the changed version of the tests to validate the new API.

Thank you, @nanuxbe! 😃 Since this is my first time handling such a issue, I may not have fully understood all the points you suggested.
I completely agree that refactoring should not break the existing logic. As you suggested, I will add deprecation warnings and write tests to validate the new API.
I also fully agree with all the other review points you provided.
(I feel that I may need to study more to better understand and build an improved structure.)

It might take me some time to complete this work, but I will make progress every day and ensure it’s not delayed too much. Once I believe the work is done, would it be okay to request review again from you?
I really appreciate your interest in this issue. Thank you!

@nanuxbe
Copy link
Contributor

nanuxbe commented Feb 19, 2025

It might take me some time to complete this work, but I will make progress every day and ensure it’s not delayed too much. Once I believe the work is done, would it be okay to request review again from you? I really appreciate your interest in this issue. Thank you!

Yes, feel free to ping me when you think this is ready for another pass!
As said on trac, it's the least I can do ;-)

@Antoliny0919
Copy link
Contributor Author

Antoliny0919 commented Feb 21, 2025

Hello @nanuxbe !
you suggested in this ticket(24947) is about making the filter functionality used in the Admin available as a new API for Django users.

However, the ticket(36175) I am currently working on does not aim to fully expose Pagination as an API. My goal has been to make it customizable, extendable, and reusable within the Admin.

One thing I'm wondering about is whether the Pagination I am working on should also be made into a public API, as suggested in the ticket(24917)?
(e.g., django.core.pagination)

@Antoliny0919 Antoliny0919 force-pushed the ticket_36175 branch 3 times, most recently from 9b69465 to 364bdd3 Compare February 23, 2025 11:57
@Antoliny0919 Antoliny0919 requested a review from nanuxbe February 23, 2025 13:09
@nanuxbe
Copy link
Contributor

nanuxbe commented Feb 24, 2025

@Antoliny0919 I am not sure what you are asking about. Paginator as it stands before this PR is already a public pagination tool, which can be used in CBVs and regular views (cf: https://docs.djangoproject.com/en/5.1/topics/pagination/#paginating-a-listview). Its API should stay stable (ie: you may add to it but should not remove/change).

AFAICT what you are working on here is a wrapper around Paginator for use in the admin so that several things there have the same pagination. I am not sure what benefit there would be to make that API available outside of the admin and make it available outside of the admin, can you elaborate on this?

@Antoliny0919
Copy link
Contributor Author

Antoliny0919 commented Feb 24, 2025

@nanuxbe Ah,,, I think I misunderstood. Your understanding is correct. I should focus on this task regardless of ticket(24947). sorry about that. 🥲

I haven't worked on the documentation yet, but if you don't mind, could you review the work I've done so far?
Would handling the deprecation as you previously suggested work like this?

@nanuxbe
Copy link
Contributor

nanuxbe commented Feb 24, 2025

@Antoliny0919 Sure. My day today is going to get quite busy in about ten minutes though, so I'll probably only be able to give it a good review tomorrow or Wednesday

Copy link
Contributor

@sarahboyce sarahboyce left a comment

Choose a reason for hiding this comment

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

Can you pull out the get_query_string refactor to a separate commit?

Instead of defining a new utility, we should use querystring
I think we should also consider using querystring directly in the template, and pass the arguments in the context

"replaced by an method in the Pagination class.",
RemovedInDjango70Warning,
stacklevel=2,
)
Copy link
Contributor

Choose a reason for hiding this comment

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

@Antoliny0919 the deprecation warnings look good to me

@nanuxbe
Copy link
Contributor

nanuxbe commented Mar 19, 2025

@Antoliny0919 No pressure but I see there is no more activity here, is this ready for another review round?

@Antoliny0919
Copy link
Contributor Author

Yes @nanuxbe, there are still parts that need to be worked on after 35529 is merged, and the documentation has not been updated yet(This is the reason why I haven't marked the ticket as ready for review.). However, if you are willing to review it, I would appreciate it.

Return an instance of the `Pagination` class to be
used for paginating the queryset.
"""
Pagination = self.get_pagination_class(request)
Copy link
Contributor

Choose a reason for hiding this comment

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

Since you are calling get_pagination_class without passing any kwargs, and get_pagination_class itself doesn't use the kwargs, I wonder if they should be removed from the method definition

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry for the late reply. I agree, it looks like kwargs aren’t necessary here.


context = {
**self.admin_site.each_context(request),
"title": _("Change history: %s") % obj,
"subtitle": None,
"action_list": page_obj,
"page_range": page_range,
Copy link
Contributor

Choose a reason for hiding this comment

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

These 3 params (page_range, page_var and pagination_required should probably be left in the context with a deprecation warning if accessed.
The use case here is that there are quite a few admin themes around and removing variables from a context will probably break a few of those

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree. Suddenly removing them from the context could cause problems for people who were previously using those context variables.
However, I haven’t found a way to show a deprecation warning when those variables are accessed from the context.
Should I implement it myself? I’ll look into it a bit more for now!

return format_html("{} ", self.paginator.ELLIPSIS)
elif i == self.page_num:
return format_html('<span class="this-page">{}</span> ', i)
else:
Copy link
Contributor

Choose a reason for hiding this comment

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

My preference here would be to drop the else and un-indent the rest of the code. Ie: return early and have the main body of the function as the "default".
It's a just preference though

from django.utils.http import urlencode
from django.utils.timezone import make_aware
from django.utils.translation import gettext

# Changelist settings
ALL_VAR = "all"
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if those should be left there with a deprecation warning. I would expect one of the fellows would have an opinion about this

@Antoliny0919
Copy link
Contributor Author

Thank you for taking an interest in this work and for reviewing it @nanuxbe 🥰. I will reflect on the points you mentioned as soon as I have time.

@Antoliny0919 Antoliny0919 force-pushed the ticket_36175 branch 2 times, most recently from c794504 to f772203 Compare May 28, 2025 12:08
@cliff688 cliff688 self-requested a review May 28, 2025 12:44

@property
def pagination_required(self):
return (not self.show_all or not self.can_show_all) and self.multi_page
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
return (not self.show_all or not self.can_show_all) and self.multi_page
self.multi_page and not (self.show_all and self.can_show_all)

I think reversing the order here is more readable

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree. Thank you, cliff!

Comment on lines 48 to 51
def render_all_pages(self):
"""
Generate the HTML for all page links in the pagination.
"""
Copy link
Member

Choose a reason for hiding this comment

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

This name doesn't sound property-like. It sounds more like it does something, rather than representing something. Maybe something like:

Suggested change
def render_all_pages(self):
"""
Generate the HTML for all page links in the pagination.
"""
def all_rendered_pages(self):
"""HTML for all the pages in the pagination."""

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Since it is provided as a property rather than a method, the name you suggested is more appropriate.
Hmm, but I also think it might be better if it were provided as a method rather than a property 🧐

Comment on lines 46 to 47
"paginator_number template tag has been "
"replaced by an method in the Pagination class.",
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
"paginator_number template tag has been "
"replaced by an method in the Pagination class.",
"paginator_number template tag has been replaced by an method in the Pagination "
"class.",

SEARCH_VAR = "q"
ERROR_FLAG = "e"


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

Comment on lines 97 to 100
self.list_select_related = list_select_related
self.list_per_page = list_per_page
self.list_max_show_all = list_max_show_all
self.list_select_related = list_select_related
Copy link
Member

Choose a reason for hiding this comment

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

You can revert this 👍🏾

Copy link
Member

Choose a reason for hiding this comment

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

I think pagination.py is a better name here

Comment on lines 322 to 323
"ChangeList().page_num attribute is deprecated. "
"use pagination.page_num instead.",
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
"ChangeList().page_num attribute is deprecated. "
"use pagination.page_num instead.",
"ChangeList().page_num attribute is deprecated. Use "
pagination.page_num instead.",

Comment on lines 336 to 337
"ChangeList().show_all attribute is deprecated. "
"use pagination.show_all instead.",
Copy link
Member

Choose a reason for hiding this comment

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

Ditto, as above. Capitalise second sentence

Copy link
Member

Choose a reason for hiding this comment

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

Also in the other warnings

all" admin change list page. The admin will display a "Show all" link on the
change list only if the total result count is less than or equal to this
setting. By default, this is set to ``200``.
all" admin page(change list, history). The admin will display a "Show all"
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
all" admin page(change list, history). The admin will display a "Show all"
all" admin page (change list, history). The admin will display a "Show all"
```suggestion


.. attribute:: ModelAdmin.list_per_page

Set ``list_per_page`` to control how many items appear on each paginated
admin change list page. By default, this is set to ``100``.
admin page(change list, history). By default, this is set to ``100``.
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
admin page(change list, history). By default, this is set to ``100``.
admin page (change list, history). By default, this is set to ``100``.

Copy link
Member

@cliff688 cliff688 left a comment

Choose a reason for hiding this comment

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

Thanks @Antoliny0919 for your dedication to this work ⭐ !

I left some comments (from an incomplete review).

I think you should split your commit so that the deprecations are in a separate first commit and the fix in a different commit. That will make this a bit easier to review.

@Antoliny0919
Copy link
Contributor Author

Thanks @Antoliny0919 for your dedication to this work ⭐ !

I left some comments (from an incomplete review).

I think you should split your commit so that the deprecations are in a separate first commit and the fix in a different commit. That will make this a bit easier to review.

Thank you for reviewing this work @cliff688 .
I had put this aside for a while, but now I’ll have to pick it up again!

@Antoliny0919
Copy link
Contributor Author

Could you please add the Selenium label? 😎

@cliff688 cliff688 added the selenium Apply to have Selenium tests run on a PR label Jun 14, 2025
@sarahboyce
Copy link
Contributor

@Antoliny0919 do you mind resolving the conflicts before this goes through another round of review? ⭐

@Antoliny0919
Copy link
Contributor Author

@Antoliny0919 do you mind resolving the conflicts before this goes through another round of review? ⭐

Nice!! I'm glad this work can be reviewed!
I'm preparing another ticket PR, so I'll submit that first and then try to get this done within this week as much as possible :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
screenshots 🖼️ selenium Apply to have Selenium tests run on a PR
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants