Skip to content

Commit

Permalink
Merge pull request #2479 from pallets-eco/page-size
Browse files Browse the repository at this point in the history
Allow customising `page_size` options, and prevent arbitrary page sizes
  • Loading branch information
samuelhwilliams authored Jul 24, 2024
2 parents 16f688b + eca8e8b commit 2cfe747
Show file tree
Hide file tree
Showing 8 changed files with 250 additions and 10 deletions.
3 changes: 3 additions & 0 deletions examples/sqla/admin/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ def is_numberic_validator(form, field):

class UserAdmin(sqla.ModelView):

can_set_page_size = True
page_size = 5
page_size_options = (5,10,15)
can_view_details = True # show a modal dialog with records details
action_disallowed_list = ['delete', ]

Expand Down
2 changes: 1 addition & 1 deletion flask_admin/contrib/appengine/view.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ def get_list(self, page, sort_field, sort_desc, search, filters,
order_field = -order_field
q = q.order(order_field)

if not page_size:
if page_size is None:
page_size = self.page_size

results = q.fetch(page_size, offset=page * page_size)
Expand Down
24 changes: 21 additions & 3 deletions flask_admin/model/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -788,6 +788,11 @@ class MyModelView(BaseModelView):
Allows to select page size via dropdown list
"""

page_size_options: tuple = (20, 50, 100)
"""
Sets the page size options available, if `can_set_page_size` is True
"""

def __init__(self, model,
name=None, category=None, endpoint=None, url=None, static_folder=None,
menu_class_name=None, menu_icon_type=None, menu_icon_value=None):
Expand Down Expand Up @@ -833,6 +838,12 @@ def __init__(self, model,
# Scaffolding
self._refresh_cache()

if self.can_set_page_size and self.page_size not in self.page_size_options:
warnings.warn(
f"{self.page_size=} is not in {self.page_size_options=}",
UserWarning
)

# Endpoint
def _get_endpoint(self, endpoint):
if endpoint:
Expand Down Expand Up @@ -1507,6 +1518,14 @@ def _get_default_order(self):

return None

def get_safe_page_size(self, page_size):
safe_page_size = self.page_size

if self.can_set_page_size and page_size in self.page_size_options:
safe_page_size = page_size

return safe_page_size

# Database-related API
def get_list(self, page, sort_field, sort_desc, search, filters,
page_size=None):
Expand Down Expand Up @@ -1806,8 +1825,7 @@ def _get_list_url(self, view_args):
kwargs = dict(page=page, sort=view_args.sort, desc=desc, search=view_args.search)
kwargs.update(view_args.extra_args)

if view_args.page_size:
kwargs['page_size'] = view_args.page_size
kwargs['page_size'] = self.get_safe_page_size(view_args.page_size)

kwargs.update(self._get_filters(view_args.filters))

Expand Down Expand Up @@ -1989,7 +2007,7 @@ def index_view(self):
sort_column = sort_column[0]

# Get page size
page_size = view_args.page_size or self.page_size
page_size = self.get_safe_page_size(view_args.page_size)

# Get count and data
count, data = self.get_list(view_args.page, sort_column, view_args.sort_desc,
Expand Down
8 changes: 4 additions & 4 deletions flask_admin/templates/bootstrap4/admin/model/layout.html
Original file line number Diff line number Diff line change
Expand Up @@ -97,13 +97,13 @@
</form>
{% endmacro %}

{% macro page_size_form(generator, btn_class='nav-link dropdown-toggle') %}
{% macro page_size_form(generator, page_size_options, btn_class='nav-link dropdown-toggle') %}
<a class="{{ btn_class }}" data-toggle="dropdown" href="javascript:void(0)">
{{ page_size }} {{ _gettext('items') }}<b class="caret"></b>
</a>
<div class="dropdown-menu">
<a class="dropdown-item{% if page_size == 20 %} active{% endif %}" href="{{ generator(20) }}">20 {{ _gettext('items') }}</a>
<a class="dropdown-item{% if page_size == 50 %} active{% endif %}" href="{{ generator(50) }}">50 {{ _gettext('items') }}</a>
<a class="dropdown-item{% if page_size == 100 %} active{% endif %}" href="{{ generator(100) }}">100 {{ _gettext('items') }}</a>
{% for option in page_size_options %}
<a class="dropdown-item{% if page_size == option %} active{% endif %}" href="{{ generator(option) }}">{{ _ngettext('{} item'.format(option), '{} items'.format(option), option) }}</a>
{% endfor %}
</div>
{% endmacro %}
4 changes: 2 additions & 2 deletions flask_admin/templates/bootstrap4/admin/model/list.html
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@

{% if can_set_page_size %}
<li class="nav-item dropdown">
{{ model_layout.page_size_form(page_size_url) }}
{{ model_layout.page_size_form(page_size_url, admin_view.page_size_options) }}
</li>
{% endif %}

Expand Down Expand Up @@ -190,7 +190,7 @@
{{ lib.form_js() }}
<script src="{{ admin_static.url(filename='admin/js/bs4_modal.js', v='1.0.0') }}"></script>
<script src="{{ admin_static.url(filename='admin/js/bs4_filters.js', v='1.0.0') }}"></script>


{{ actionlib.script(_gettext('Please select at least one record.'),
actions,
Expand Down
73 changes: 73 additions & 0 deletions flask_admin/tests/mongoengine/test_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -1136,6 +1136,79 @@ def get_count_query(self):
assert count is None


def test_customising_page_size(app, db, admin):
with app.app_context():
M1, _ = create_models(db)

instances = [M1(test1=f'instance-{x+1:03d}') for x in range(101)]
for instance in instances:
instance.save()

view1 = CustomModelView(M1, endpoint='view1', page_size=20, can_set_page_size=False)
admin.add_view(view1)

view2 = CustomModelView(M1, db, endpoint='view2', page_size=5, can_set_page_size=False)
admin.add_view(view2)

view3 = CustomModelView(M1, db, endpoint='view3', page_size=20, can_set_page_size=True)
admin.add_view(view3)

view4 = CustomModelView(M1, db, endpoint='view4', page_size=5, page_size_options=(5, 10, 15), can_set_page_size=True)
admin.add_view(view4)

client = app.test_client()

rv = client.get('/admin/view1/')
assert 'instance-020' in rv.text
assert 'instance-021' not in rv.text

# `can_set_page_size=False`, so only the default of 20 is available.
rv = client.get('/admin/view1/?page_size=50')
assert 'instance-020' in rv.text
assert 'instance-021' not in rv.text

# Check view2, which has `page_size=5` to change the default page size
rv = client.get('/admin/view2/')
assert 'instance-005' in rv.text
assert 'instance-006' not in rv.text

# Check view3, which has `can_set_page_size=True`
rv = client.get('/admin/view3/')
assert 'instance-020' in rv.text
assert 'instance-021' not in rv.text

rv = client.get('/admin/view3/?page_size=50')
assert 'instance-050' in rv.text
assert 'instance-051' not in rv.text

rv = client.get('/admin/view3/?page_size=100')
assert 'instance-100' in rv.text
assert 'instance-101' not in rv.text

# Invalid page sizes are reset to the default
rv = client.get('/admin/view3/?page_size=1')
assert 'instance-020' in rv.text
assert 'instance-021' not in rv.text

# Check view4, which has custom `page_size_options`
rv = client.get('/admin/view4/')
assert 'instance-005' in rv.text
assert 'instance-006' not in rv.text

# Invalid page sizes are reset to the default
rv = client.get('/admin/view4/?page_size=1')
assert 'instance-005' in rv.text
assert 'instance-006' not in rv.text

rv = client.get('/admin/view4/?page_size=10')
assert 'instance-010' in rv.text
assert 'instance-011' not in rv.text

rv = client.get('/admin/view4/?page_size=15')
assert 'instance-015' in rv.text
assert 'instance-016' not in rv.text


def test_export_csv(app, db, admin):
Model1, Model2 = create_models(db)

Expand Down
73 changes: 73 additions & 0 deletions flask_admin/tests/peeweemodel/test_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -1020,6 +1020,79 @@ class Model2(BaseModel):
assert mdl.model1.test1 == u'first'


def test_customising_page_size(app, db, admin):
with app.app_context():
M1, _ = create_models(db)

instances = [M1(f'instance-{x+1:03d}') for x in range(101)]
for instance in instances:
instance.save()

view1 = CustomModelView(M1, endpoint='view1', page_size=20, can_set_page_size=False)
admin.add_view(view1)

view2 = CustomModelView(M1, db, endpoint='view2', page_size=5, can_set_page_size=False)
admin.add_view(view2)

view3 = CustomModelView(M1, db, endpoint='view3', page_size=20, can_set_page_size=True)
admin.add_view(view3)

view4 = CustomModelView(M1, db, endpoint='view4', page_size=5, page_size_options=(5, 10, 15), can_set_page_size=True)
admin.add_view(view4)

client = app.test_client()

rv = client.get('/admin/view1/')
assert 'instance-020' in rv.text
assert 'instance-021' not in rv.text

# `can_set_page_size=False`, so only the default of 20 is available.
rv = client.get('/admin/view1/?page_size=50')
assert 'instance-020' in rv.text
assert 'instance-021' not in rv.text

# Check view2, which has `page_size=5` to change the default page size
rv = client.get('/admin/view2/')
assert 'instance-005' in rv.text
assert 'instance-006' not in rv.text

# Check view3, which has `can_set_page_size=True`
rv = client.get('/admin/view3/')
assert 'instance-020' in rv.text
assert 'instance-021' not in rv.text

rv = client.get('/admin/view3/?page_size=50')
assert 'instance-050' in rv.text
assert 'instance-051' not in rv.text

rv = client.get('/admin/view3/?page_size=100')
assert 'instance-100' in rv.text
assert 'instance-101' not in rv.text

# Invalid page sizes are reset to the default
rv = client.get('/admin/view3/?page_size=1')
assert 'instance-020' in rv.text
assert 'instance-021' not in rv.text

# Check view4, which has custom `page_size_options`
rv = client.get('/admin/view4/')
assert 'instance-005' in rv.text
assert 'instance-006' not in rv.text

# Invalid page sizes are reset to the default
rv = client.get('/admin/view4/?page_size=1')
assert 'instance-005' in rv.text
assert 'instance-006' not in rv.text

rv = client.get('/admin/view4/?page_size=10')
assert 'instance-010' in rv.text
assert 'instance-011' not in rv.text

rv = client.get('/admin/view4/?page_size=15')
assert 'instance-015' in rv.text
assert 'instance-016' not in rv.text


def test_export_csv(app, db, admin):
Model1, Model2 = create_models(db)

Expand Down
73 changes: 73 additions & 0 deletions flask_admin/tests/sqla/test_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -2439,6 +2439,79 @@ def get_count_query(self):
assert count is None


def test_customising_page_size(app, db, admin):
with app.app_context():
M1, _ = create_models(db)

db.session.add_all(
[M1(str(f'instance-{x+1:03d}')) for x in range(101)]
)

view1 = CustomModelView(M1, db.session, endpoint='view1', page_size=20, can_set_page_size=False)
admin.add_view(view1)

view2 = CustomModelView(M1, db.session, endpoint='view2', page_size=5, can_set_page_size=False)
admin.add_view(view2)

view3 = CustomModelView(M1, db.session, endpoint='view3', page_size=20, can_set_page_size=True)
admin.add_view(view3)

view4 = CustomModelView(M1, db.session, endpoint='view4', page_size=5, page_size_options=(5, 10, 15), can_set_page_size=True)
admin.add_view(view4)

client = app.test_client()

rv = client.get('/admin/view1/')
assert 'instance-020' in rv.text
assert 'instance-021' not in rv.text

# `can_set_page_size=False`, so only the default of 20 is available.
rv = client.get('/admin/view1/?page_size=50')
assert 'instance-020' in rv.text
assert 'instance-021' not in rv.text

# Check view2, which has `page_size=5` to change the default page size
rv = client.get('/admin/view2/')
assert 'instance-005' in rv.text
assert 'instance-006' not in rv.text

# Check view3, which has `can_set_page_size=True`
rv = client.get('/admin/view3/')
assert 'instance-020' in rv.text
assert 'instance-021' not in rv.text

rv = client.get('/admin/view3/?page_size=50')
assert 'instance-050' in rv.text
assert 'instance-051' not in rv.text

rv = client.get('/admin/view3/?page_size=100')
assert 'instance-100' in rv.text
assert 'instance-101' not in rv.text

# Invalid page sizes are reset to the default
rv = client.get('/admin/view3/?page_size=1')
assert 'instance-020' in rv.text
assert 'instance-021' not in rv.text

# Check view4, which has custom `page_size_options`
rv = client.get('/admin/view4/')
assert 'instance-005' in rv.text
assert 'instance-006' not in rv.text

# Invalid page sizes are reset to the default
rv = client.get('/admin/view4/?page_size=1')
assert 'instance-005' in rv.text
assert 'instance-006' not in rv.text

rv = client.get('/admin/view4/?page_size=10')
assert 'instance-010' in rv.text
assert 'instance-011' not in rv.text

rv = client.get('/admin/view4/?page_size=15')
assert 'instance-015' in rv.text
assert 'instance-016' not in rv.text


def test_unlimited_page_size(app, db, admin):
with app.app_context():
M1, _ = create_models(db)
Expand Down

0 comments on commit 2cfe747

Please sign in to comment.