Skip to content

Commit 5b0e848

Browse files
Phinart98lwasser
authored andcommitted
Add pagination, year filtering, and styling enhancements to blog and events pages
- Add pagination (12 posts/page for blog, 15 events/page) - Add year filtering with scroll position preservation - Add header images with overlays on index pages - Add Upcoming Events section with purple sidebar cards - Fix rich text formatting by installing Tailwind Typography plugin - Add management command to generate test data
1 parent 1d80b7b commit 5b0e848

File tree

13 files changed

+445
-35
lines changed

13 files changed

+445
-35
lines changed

core/views.py

Lines changed: 76 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from django.shortcuts import render, get_object_or_404
2+
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
23
import logging
34

45
from .utils import (
@@ -67,7 +68,8 @@ def blog_index(request):
6768
Blog index view for PyOpenSci.
6869
6970
Static Django view that queries Wagtail BlogPage instances
70-
to display all blog posts in a consistent index page.
71+
to display all blog posts in a consistent index page with pagination
72+
and optional year filtering.
7173
7274
Parameters
7375
----------
@@ -77,15 +79,44 @@ def blog_index(request):
7779
Returns
7880
-------
7981
HttpResponse
80-
Rendered blog index page with blog posts.
82+
Rendered blog index page with paginated blog posts.
8183
"""
84+
# Get year filter from query params
85+
year_filter = request.GET.get('year')
86+
87+
# Base queryset
8288
blog_posts = BlogPage.objects.live().select_related('author').order_by('-date')
8389

90+
# Apply year filter if provided
91+
if year_filter and year_filter.isdigit():
92+
blog_posts = blog_posts.filter(date__year=int(year_filter))
93+
94+
# Get available years for filter dropdown
95+
available_years = (
96+
BlogPage.objects.live()
97+
.dates('date', 'year', order='DESC')
98+
)
99+
100+
# Pagination: 12 posts per page
101+
paginator = Paginator(blog_posts, 12)
102+
page = request.GET.get('page')
103+
104+
try:
105+
paginated_posts = paginator.page(page)
106+
except PageNotAnInteger:
107+
# If page is not an integer, deliver first page
108+
paginated_posts = paginator.page(1)
109+
except EmptyPage:
110+
# If page is out of range, deliver last page of results
111+
paginated_posts = paginator.page(paginator.num_pages)
112+
84113
context = {
85114
'page_title': 'pyOpenSci Blog',
86115
'hero_title': 'pyOpenSci Blog',
87116
'hero_subtitle': 'Here we will both post updates about pyOpenSci and also highlight contributors. We will also highlight new packages that have been reviewed and accepted into the pyOpenSci ecosystem.',
88-
'blog_posts': blog_posts,
117+
'blog_posts': paginated_posts,
118+
'available_years': available_years,
119+
'selected_year': year_filter,
89120
}
90121
return render(request, 'core/blog_index.html', context)
91122

@@ -95,7 +126,8 @@ def events_index(request):
95126
Events index view for PyOpenSci.
96127
97128
Static Django view that queries Wagtail EventPage instances
98-
to display all events in a consistent index page.
129+
to display all events in a consistent index page with pagination
130+
and optional year filtering.
99131
100132
Parameters
101133
----------
@@ -105,15 +137,52 @@ def events_index(request):
105137
Returns
106138
-------
107139
HttpResponse
108-
Rendered events index page with events.
140+
Rendered events index page with paginated events.
109141
"""
142+
# Get year filter from query params
143+
year_filter = request.GET.get('year')
144+
145+
# Base queryset
110146
events = EventPage.objects.live().select_related('author').prefetch_related('tags').order_by('-start_date')
111147

148+
# Apply year filter if provided
149+
if year_filter and year_filter.isdigit():
150+
events = events.filter(start_date__year=int(year_filter))
151+
152+
# Get available years for filter dropdown
153+
available_years = (
154+
EventPage.objects.live()
155+
.dates('start_date', 'year', order='DESC')
156+
)
157+
158+
# Pagination: 15 events per page
159+
paginator = Paginator(events, 15)
160+
page = request.GET.get('page')
161+
162+
try:
163+
paginated_events = paginator.page(page)
164+
except PageNotAnInteger:
165+
# If page is not an integer, deliver first page
166+
paginated_events = paginator.page(1)
167+
except EmptyPage:
168+
# If page is out of range, deliver last page of results
169+
paginated_events = paginator.page(paginator.num_pages)
170+
171+
# Separate upcoming and past events
172+
from django.utils import timezone
173+
today = timezone.now().date()
174+
175+
upcoming_events = EventPage.objects.live().filter(start_date__gte=today).select_related('author').prefetch_related('tags').order_by('start_date')
176+
112177
context = {
113178
'page_title': 'pyOpenSci Events',
114179
'hero_title': 'pyOpenSci Events',
115-
'hero_subtitle': 'Join us for workshops, webinars, and community events. Connect with the scientific Python community and learn about open source best practices.',
116-
'events': events,
180+
'hero_subtitle': 'pyOpenSci holds events that support scientists developing open science skills.',
181+
'events': paginated_events,
182+
'upcoming_events': upcoming_events,
183+
'available_years': available_years,
184+
'selected_year': year_filter,
185+
'today': today,
117186
}
118187
return render(request, 'core/events_index.html', context)
119188

package-lock.json

Lines changed: 28 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"build-prod": "tailwindcss -i ./static/css/input.css -o ./static/css/styles.css --minify"
88
},
99
"devDependencies": {
10+
"@tailwindcss/typography": "^0.5.19",
1011
"tailwindcss": "^3.4.0"
1112
}
12-
}
13+
}

publications/management/__init__.py

Whitespace-only changes.

publications/management/commands/__init__.py

Whitespace-only changes.
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
from django.core.management.base import BaseCommand
2+
from django.utils import timezone
3+
from datetime import timedelta
4+
from publications.models import BlogPage, EventPage, Author
5+
from wagtail.models import Page
6+
7+
8+
class Command(BaseCommand):
9+
help = "Create dummy blog posts and events for testing pagination and filtering"
10+
11+
def add_arguments(self, parser):
12+
parser.add_argument(
13+
'--blog-posts',
14+
type=int,
15+
default=25,
16+
help='Number of blog posts to create (default: 25)'
17+
)
18+
parser.add_argument(
19+
'--events',
20+
type=int,
21+
default=20,
22+
help='Number of events to create (default: 20)'
23+
)
24+
parser.add_argument(
25+
'--delete',
26+
action='store_true',
27+
help='Delete existing dummy posts before creating new ones'
28+
)
29+
30+
def handle(self, *args, **options):
31+
num_blog_posts = options['blog_posts']
32+
num_events = options['events']
33+
delete_existing = options['delete']
34+
35+
# Get or create a default author
36+
author, created = Author.objects.get_or_create(
37+
slug='test-author',
38+
defaults={
39+
'name': 'Test Author',
40+
'bio': 'This is a test author for dummy content.',
41+
}
42+
)
43+
44+
if created:
45+
self.stdout.write(self.style.SUCCESS(f'Created test author: {author.name}'))
46+
47+
# Get the home page to use as parent
48+
home_page = Page.objects.get(depth=2).specific
49+
50+
# Delete existing dummy posts if requested
51+
if delete_existing:
52+
deleted_blogs = BlogPage.objects.filter(title__startswith='Test Blog Post').delete()
53+
deleted_events = EventPage.objects.filter(title__startswith='Test Event').delete()
54+
self.stdout.write(self.style.WARNING(
55+
f'Deleted {deleted_blogs[0]} blog posts and {deleted_events[0]} events'
56+
))
57+
58+
# Create blog posts
59+
self.stdout.write(self.style.NOTICE(f'\nCreating {num_blog_posts} blog posts...'))
60+
current_date = timezone.now()
61+
62+
for i in range(1, num_blog_posts + 1):
63+
# Distribute posts across multiple years
64+
year_offset = (i - 1) // 10 # 10 posts per year
65+
month_offset = (i - 1) % 12
66+
post_date = current_date - timedelta(days=365 * year_offset + 30 * month_offset)
67+
68+
blog_post = BlogPage(
69+
title=f'Test Blog Post {i}',
70+
slug=f'test-blog-post-{i}',
71+
date=post_date,
72+
author=author,
73+
excerpt=f'This is a test excerpt for blog post {i}. '
74+
'It provides a brief overview of the post content.',
75+
body=f'<p>This is the body content for test blog post {i}.</p>'
76+
'<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. '
77+
'Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>',
78+
enable_comments=True,
79+
)
80+
home_page.add_child(instance=blog_post)
81+
blog_post.save_revision().publish()
82+
83+
if i % 5 == 0:
84+
self.stdout.write(f' Created {i}/{num_blog_posts} blog posts...')
85+
86+
self.stdout.write(self.style.SUCCESS(f'✓ Created {num_blog_posts} blog posts'))
87+
88+
# Create events
89+
self.stdout.write(self.style.NOTICE(f'\nCreating {num_events} events...'))
90+
91+
event_types = ['workshop', 'webinar', 'conference', 'meetup', 'other']
92+
93+
for i in range(1, num_events + 1):
94+
# Mix of past and future events
95+
if i % 2 == 0:
96+
# Future event
97+
event_date = current_date + timedelta(days=30 * i)
98+
else:
99+
# Past event
100+
event_date = current_date - timedelta(days=30 * i)
101+
102+
event_type = event_types[i % len(event_types)]
103+
104+
event = EventPage(
105+
title=f'Test Event {i}',
106+
slug=f'test-event-{i}',
107+
date=current_date,
108+
author=author,
109+
start_date=event_date.date(),
110+
end_date=(event_date + timedelta(days=1)).date() if i % 3 == 0 else event_date.date(),
111+
location='Online' if i % 2 == 0 else 'San Francisco, CA',
112+
event_type=event_type,
113+
excerpt=f'This is a test excerpt for event {i}. Join us for this exciting event!',
114+
body=f'<p>This is the body content for test event {i}.</p>'
115+
'<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>',
116+
enable_comments=True,
117+
)
118+
home_page.add_child(instance=event)
119+
event.save_revision().publish()
120+
121+
if i % 5 == 0:
122+
self.stdout.write(f' Created {i}/{num_events} events...')
123+
124+
self.stdout.write(self.style.SUCCESS(f'✓ Created {num_events} events'))
125+
126+
# Summary
127+
self.stdout.write(self.style.SUCCESS(
128+
f'\n✅ Successfully created {num_blog_posts} blog posts and {num_events} events!'
129+
))
130+
self.stdout.write(self.style.NOTICE(
131+
'\nYou can now test:\n'
132+
' - Blog pagination at /blog/ (12 posts per page)\n'
133+
' - Events pagination at /events/ (15 events per page)\n'
134+
' - Year filtering on blog page\n'
135+
))
162 KB
Loading
9.73 KB
Loading

tailwind.config.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,7 @@ module.exports = {
2525
}
2626
},
2727
},
28-
plugins: [],
28+
plugins: [
29+
require('@tailwindcss/typography'),
30+
],
2931
}

0 commit comments

Comments
 (0)