Skip to content

Commit bf59a8a

Browse files
authored
Story 2377: Webpage Integration: Implement Pagination to Posts Feed Page (#2392)
1 parent e5ea923 commit bf59a8a

9 files changed

Lines changed: 279 additions & 1 deletion

File tree

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
from django import template
2+
from django.core.paginator import Paginator
3+
4+
register = template.Library()
5+
6+
7+
@register.simple_tag(takes_context=True)
8+
def resolve_pagination(context, current=None, total=None):
9+
"""Return a Django Page object resolved from context or explicit overrides.
10+
11+
In a ListView context, returns page_obj directly.
12+
In isolated/demo usage, creates a real Paginator from the given values.
13+
"""
14+
page_obj = context.get("page_obj")
15+
16+
if isinstance(current, int) and isinstance(total, int):
17+
paginator = Paginator(range(1, total + 1), 1)
18+
return paginator.page(max(1, min(current, paginator.num_pages)))
19+
20+
if page_obj is not None:
21+
return page_obj
22+
23+
return Paginator([], 1).page(1)
24+
25+
26+
@register.simple_tag
27+
def pagination_range(page, window=2):
28+
"""Return an elided page range for the given Page object.
29+
30+
Always uses an ellipsis for any gap (even a single page), unlike Django's
31+
built-in get_elided_page_range which fills single-page gaps with the page
32+
number. Each element is either an integer or '…'.
33+
"""
34+
num_pages = page.paginator.num_pages
35+
current = page.number
36+
37+
if num_pages <= 1:
38+
return list(range(1, num_pages + 1))
39+
40+
window_start = max(2, current - window)
41+
window_end = min(num_pages - 1, current + window)
42+
43+
items = [1]
44+
if window_start > 2:
45+
items.append("…")
46+
items.extend(range(window_start, window_end + 1))
47+
if window_end < num_pages - 1:
48+
items.append("…")
49+
items.append(num_pages)
50+
return items

core/views.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2134,4 +2134,22 @@ def get_context_data(self, **kwargs):
21342134
v3_paths.sort(key=lambda p: p["class_name"])
21352135
context["v3_paths"] = v3_paths
21362136

2137+
_demo_total_choices = [1, 3, 5, 24]
2138+
try:
2139+
demo_total = int(self.request.GET.get("total", 24))
2140+
if demo_total not in _demo_total_choices:
2141+
demo_total = 24
2142+
except (ValueError, TypeError):
2143+
demo_total = 24
2144+
try:
2145+
demo_page = int(self.request.GET.get("page", 1))
2146+
demo_page = max(1, min(demo_page, demo_total))
2147+
except (ValueError, TypeError):
2148+
demo_page = 1
2149+
context["demo_pagination"] = {
2150+
"current": demo_page,
2151+
"total": demo_total,
2152+
"total_choices": _demo_total_choices,
2153+
}
2154+
21372155
return context

news/views.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ class EntryListView(V3Mixin, ListView):
8585
template_name = "news/list.html"
8686
v3_template_name = "v3/posts_list.html"
8787
ordering = ["-publish_at"]
88-
paginate_by = None # XXX: use pagination in the template! Issue #377
88+
paginate_by = 10
8989
context_object_name = "entry_list" # Ensure children use the same name
9090
header_text = "Latest Posts"
9191
filter_value = "all"

static/css/v3/components.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,4 @@
5555
@import "./library-discovery-card.css";
5656
@import "./help-card.css";
5757
@import "./quick-start-card.css";
58+
@import "./pagination.css";

static/css/v3/pagination.css

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/*
2+
* Pagination component
3+
* Token hierarchy: semantics.css → themes.css (dark overrides)
4+
* Usage: _pagination.html
5+
*
6+
* Two navs are rendered: --wide (±2 window) and --narrow (±1 window).
7+
* CSS toggles which one is visible based on viewport width.
8+
* display:none removes both from the visual layer and the a11y tree.
9+
*/
10+
11+
.pagination {
12+
display: flex;
13+
flex-direction: row;
14+
justify-content: center;
15+
align-items: center;
16+
gap: var(--space-default);
17+
padding: var(--space-large) 0;
18+
list-style: none;
19+
margin: 0;
20+
}
21+
22+
/* ── Shared button styles ── */
23+
24+
.pagination__item {
25+
display: flex;
26+
}
27+
28+
.pagination__link,
29+
.pagination__chevron {
30+
display: flex;
31+
align-items: center;
32+
justify-content: center;
33+
height: 40px;
34+
padding: 0 var(--space-large);
35+
border: 1px solid transparent;
36+
border-radius: var(--border-radius-xl);
37+
font-family: var(--font-sans);
38+
font-size: var(--font-size-small);
39+
font-weight: var(--font-weight-regular);
40+
line-height: var(--line-height-default);
41+
color: var(--color-text-secondary);
42+
text-decoration: none;
43+
transition: background-color 0.15s ease;
44+
}
45+
46+
/* ── Page number links ── */
47+
48+
.pagination__link:hover {
49+
background-color: var(--color-surface-mid);
50+
border-color: var(--color-stroke-weak);
51+
color: var(--color-text-primary);
52+
}
53+
54+
.pagination__link--active {
55+
background-color: var(--color-surface-weak);
56+
border-color: var(--color-stroke-weak);
57+
color: var(--color-text-primary);
58+
pointer-events: none;
59+
}
60+
61+
/* ── Ellipsis ── */
62+
63+
.pagination__ellipsis {
64+
display: flex;
65+
align-items: center;
66+
justify-content: center;
67+
height: 40px;
68+
padding: 0 var(--space-default);
69+
font-family: var(--font-sans);
70+
font-size: var(--font-size-small);
71+
font-weight: var(--font-weight-regular);
72+
color: var(--color-text-tertiary);
73+
user-select: none;
74+
}
75+
76+
/* ── Chevron navigation ── */
77+
78+
.pagination__chevron {
79+
width: 20px;
80+
height: 20px;
81+
padding: 0;
82+
border: none;
83+
color: var(--color-icon-primary);
84+
}
85+
86+
.pagination__chevron--disabled {
87+
color: var(--color-text-tertiary);
88+
pointer-events: none;
89+
cursor: default;
90+
}
91+
92+
/* ── Responsive visibility ── */
93+
94+
.pagination-nav--narrow {
95+
display: none;
96+
}
97+
98+
@media (max-width: 767px) {
99+
.pagination-nav--wide {
100+
display: none;
101+
}
102+
103+
.pagination-nav--narrow {
104+
display: block;
105+
}
106+
}

templates/v3/examples/_v3_example_section.html

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -915,6 +915,42 @@ <h3>{{ section_title }}</h3>
915915
</div>
916916
{% endwith %}
917917

918+
{% with section_title="Pagination" %}
919+
<div class="v3-examples-section__block" id="{{ section_title|slugify }}">
920+
<h3>{{ section_title }}</h3>
921+
<div class="v3-examples-section__example-box">
922+
<form method="get"
923+
action="{% url 'v3-demo-components' %}#pagination"
924+
class="v3-examples-section__stats-lookup"
925+
style="display: flex;
926+
gap: var(--space-default, 8px);
927+
align-items: center;
928+
flex-wrap: wrap;
929+
margin-bottom: var(--space-xlarge)">
930+
<label for="pagination-total-select"
931+
style="color: var(--color-text-secondary); font-size: var(--font-size-small);">Total pages</label>
932+
<select id="pagination-total-select"
933+
name="total"
934+
onchange="this.form.submit()"
935+
style="padding: var(--space-medium);
936+
border: 1px solid var(--color-stroke-mid);
937+
border-radius: var(--border-radius-s, 4px);
938+
background: var(--color-surface-page);
939+
color: var(--color-text-primary)">
940+
{% for choice in demo_pagination.total_choices %}
941+
<option value="{{ choice }}"{% if choice == demo_pagination.total %} selected{% endif %}>
942+
{{ choice }} page{{ choice|pluralize }}
943+
</option>
944+
{% endfor %}
945+
</select>
946+
</form>
947+
<div id="pagination">
948+
{% include "v3/includes/_pagination.html" with pagination_current=demo_pagination.current pagination_total=demo_pagination.total pagination_anchor="pagination" %}
949+
</div>
950+
</div>
951+
</div>
952+
{% endwith %}
953+
918954
{% comment %}
919955
Position Fixed dark mode toggle for quick theme switching without scrolling to the header.
920956
{% endcomment %}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{% load pagination_tags %}
2+
{% comment %}
3+
Pagination component.
4+
5+
Standard usage (from ListView — page_obj and paginator are in context):
6+
{% include "v3/includes/_pagination.html" %}
7+
8+
Isolated / demo usage:
9+
{% include "v3/includes/_pagination.html" with pagination_current=5 pagination_total=24 %}
10+
11+
Parameters:
12+
pagination_current int – current page number (required for isolated use)
13+
pagination_total int – total number of pages (required for isolated use)
14+
pagination_anchor str – optional HTML id; appended as #fragment to every
15+
page link so the browser scrolls back to the component
16+
{% endcomment %}
17+
18+
{% resolve_pagination pagination_current pagination_total as pag %}
19+
{% pagination_range pag 2 as pages_wide %}
20+
{% pagination_range pag 1 as pages_narrow %}
21+
{% if pag.has_previous %}{% querystring page=pag.previous_page_number as prev_url %}{% endif %}
22+
{% if pag.has_next %}{% querystring page=pag.next_page_number as next_url %}{% endif %}
23+
24+
{% include "v3/includes/_pagination_nav.html" with pages=pages_wide css_class="pagination-nav--wide" %}
25+
{% include "v3/includes/_pagination_nav.html" with pages=pages_narrow css_class="pagination-nav--narrow" %}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<nav class="pagination-nav {{ css_class }}" aria-label="Pagination">
2+
<ol class="pagination">
3+
4+
<li class="pagination__item">
5+
{% if pag.has_previous %}
6+
<a href="{{ prev_url }}{% if pagination_anchor %}#{{ pagination_anchor }}{% endif %}" class="pagination__chevron" aria-label="Previous page" rel="prev">
7+
{% include "includes/icon.html" with icon_name="chevron-left" icon_size=20 %}
8+
</a>
9+
{% else %}
10+
<span class="pagination__chevron pagination__chevron--disabled" aria-disabled="true" aria-label="Previous page">
11+
{% include "includes/icon.html" with icon_name="chevron-left" icon_size=20 %}
12+
</span>
13+
{% endif %}
14+
</li>
15+
16+
{% for item in pages %}
17+
<li class="pagination__item">
18+
{% if item == "…" %}
19+
<span class="pagination__ellipsis" aria-hidden="true"></span>
20+
{% elif item == pag.number %}
21+
<a href="{% querystring page=item %}{% if pagination_anchor %}#{{ pagination_anchor }}{% endif %}" class="pagination__link pagination__link--active" aria-current="page" aria-label="Page {{ item }}">{{ item }}</a>
22+
{% else %}
23+
<a href="{% querystring page=item %}{% if pagination_anchor %}#{{ pagination_anchor }}{% endif %}" class="pagination__link" aria-label="Go to page {{ item }}">{{ item }}</a>
24+
{% endif %}
25+
</li>
26+
{% endfor %}
27+
28+
<li class="pagination__item">
29+
{% if pag.has_next %}
30+
<a href="{{ next_url }}{% if pagination_anchor %}#{{ pagination_anchor }}{% endif %}" class="pagination__chevron" aria-label="Next page" rel="next">
31+
{% include "includes/icon.html" with icon_name="chevron-right" icon_size=20 %}
32+
</a>
33+
{% else %}
34+
<span class="pagination__chevron pagination__chevron--disabled" aria-disabled="true" aria-label="Next page">
35+
{% include "includes/icon.html" with icon_name="chevron-right" icon_size=20 %}
36+
</span>
37+
{% endif %}
38+
</li>
39+
40+
</ol>
41+
</nav>

templates/v3/posts_list.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ <h1>Posts</h1>
3232
</div>
3333
<div class="post-content__content_column">
3434
{% include 'v3/includes/_post_list_card.html' with heading=header_text items=entry_list only %}
35+
{% include "v3/includes/_pagination.html" %}
3536
</div>
3637
<div class="post-content__user_card_column">
3738
{% if request.user.is_authenticated %}

0 commit comments

Comments
 (0)