+ {{ event.title }} +
+ + {% if event.excerpt %} +{{ event.excerpt }}
+ {% endif %} + + + Learn More + +diff --git a/core/views.py b/core/views.py index ea04339..79d33c5 100644 --- a/core/views.py +++ b/core/views.py @@ -1,4 +1,5 @@ from django.shortcuts import render, get_object_or_404 +from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger import logging from .utils import ( @@ -67,7 +68,8 @@ def blog_index(request): Blog index view for PyOpenSci. Static Django view that queries Wagtail BlogPage instances - to display all blog posts in a consistent index page. + to display all blog posts in a consistent index page with pagination + and optional year filtering. Parameters ---------- @@ -77,15 +79,44 @@ def blog_index(request): Returns ------- HttpResponse - Rendered blog index page with blog posts. + Rendered blog index page with paginated blog posts. """ + # Get year filter from query params + year_filter = request.GET.get('year') + + # Base queryset blog_posts = BlogPage.objects.live().select_related('author').order_by('-date') + # Apply year filter if provided + if year_filter and year_filter.isdigit(): + blog_posts = blog_posts.filter(date__year=int(year_filter)) + + # Get available years for filter dropdown + available_years = ( + BlogPage.objects.live() + .dates('date', 'year', order='DESC') + ) + + # Pagination: 12 posts per page + paginator = Paginator(blog_posts, 12) + page = request.GET.get('page') + + try: + paginated_posts = paginator.page(page) + except PageNotAnInteger: + # If page is not an integer, deliver first page + paginated_posts = paginator.page(1) + except EmptyPage: + # If page is out of range, deliver last page of results + paginated_posts = paginator.page(paginator.num_pages) + context = { 'page_title': 'pyOpenSci Blog', 'hero_title': 'pyOpenSci Blog', '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.', - 'blog_posts': blog_posts, + 'blog_posts': paginated_posts, + 'available_years': available_years, + 'selected_year': year_filter, } return render(request, 'core/blog_index.html', context) @@ -95,7 +126,8 @@ def events_index(request): Events index view for PyOpenSci. Static Django view that queries Wagtail EventPage instances - to display all events in a consistent index page. + to display all events in a consistent index page with pagination + and optional year filtering. Parameters ---------- @@ -105,15 +137,52 @@ def events_index(request): Returns ------- HttpResponse - Rendered events index page with events. + Rendered events index page with paginated events. """ + # Get year filter from query params + year_filter = request.GET.get('year') + + # Base queryset events = EventPage.objects.live().select_related('author').prefetch_related('tags').order_by('-start_date') + # Apply year filter if provided + if year_filter and year_filter.isdigit(): + events = events.filter(start_date__year=int(year_filter)) + + # Get available years for filter dropdown + available_years = ( + EventPage.objects.live() + .dates('start_date', 'year', order='DESC') + ) + + # Pagination: 15 events per page + paginator = Paginator(events, 15) + page = request.GET.get('page') + + try: + paginated_events = paginator.page(page) + except PageNotAnInteger: + # If page is not an integer, deliver first page + paginated_events = paginator.page(1) + except EmptyPage: + # If page is out of range, deliver last page of results + paginated_events = paginator.page(paginator.num_pages) + + # Separate upcoming and past events + from django.utils import timezone + today = timezone.now().date() + + upcoming_events = EventPage.objects.live().filter(start_date__gte=today).select_related('author').prefetch_related('tags').order_by('start_date') + context = { 'page_title': 'pyOpenSci Events', 'hero_title': 'pyOpenSci Events', - 'hero_subtitle': 'Join us for workshops, webinars, and community events. Connect with the scientific Python community and learn about open source best practices.', - 'events': events, + 'hero_subtitle': 'pyOpenSci holds events that support scientists developing open science skills.', + 'events': paginated_events, + 'upcoming_events': upcoming_events, + 'available_years': available_years, + 'selected_year': year_filter, + 'today': today, } return render(request, 'core/events_index.html', context) diff --git a/package-lock.json b/package-lock.json index 01782b0..c8c9b5b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "pyopensci-django", "version": "1.0.0", "devDependencies": { + "@tailwindcss/typography": "^0.5.19", "tailwindcss": "^3.4.0" } }, @@ -130,6 +131,33 @@ "node": ">=14" } }, + "node_modules/@tailwindcss/typography": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz", + "integrity": "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "6.0.10" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" + } + }, + "node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/ansi-regex": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", diff --git a/package.json b/package.json index fc86e56..942b2ca 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "build-prod": "tailwindcss -i ./static/css/input.css -o ./static/css/styles.css --minify" }, "devDependencies": { + "@tailwindcss/typography": "^0.5.19", "tailwindcss": "^3.4.0" } -} \ No newline at end of file +} diff --git a/publications/management/__init__.py b/publications/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/publications/management/commands/__init__.py b/publications/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/publications/management/commands/create_dummy_posts.py b/publications/management/commands/create_dummy_posts.py new file mode 100644 index 0000000..a45d268 --- /dev/null +++ b/publications/management/commands/create_dummy_posts.py @@ -0,0 +1,135 @@ +from django.core.management.base import BaseCommand +from django.utils import timezone +from datetime import timedelta +from publications.models import BlogPage, EventPage, Author +from wagtail.models import Page + + +class Command(BaseCommand): + help = "Create dummy blog posts and events for testing pagination and filtering" + + def add_arguments(self, parser): + parser.add_argument( + '--blog-posts', + type=int, + default=25, + help='Number of blog posts to create (default: 25)' + ) + parser.add_argument( + '--events', + type=int, + default=20, + help='Number of events to create (default: 20)' + ) + parser.add_argument( + '--delete', + action='store_true', + help='Delete existing dummy posts before creating new ones' + ) + + def handle(self, *args, **options): + num_blog_posts = options['blog_posts'] + num_events = options['events'] + delete_existing = options['delete'] + + # Get or create a default author + author, created = Author.objects.get_or_create( + slug='test-author', + defaults={ + 'name': 'Test Author', + 'bio': 'This is a test author for dummy content.', + } + ) + + if created: + self.stdout.write(self.style.SUCCESS(f'Created test author: {author.name}')) + + # Get the home page to use as parent + home_page = Page.objects.get(depth=2).specific + + # Delete existing dummy posts if requested + if delete_existing: + deleted_blogs = BlogPage.objects.filter(title__startswith='Test Blog Post').delete() + deleted_events = EventPage.objects.filter(title__startswith='Test Event').delete() + self.stdout.write(self.style.WARNING( + f'Deleted {deleted_blogs[0]} blog posts and {deleted_events[0]} events' + )) + + # Create blog posts + self.stdout.write(self.style.NOTICE(f'\nCreating {num_blog_posts} blog posts...')) + current_date = timezone.now() + + for i in range(1, num_blog_posts + 1): + # Distribute posts across multiple years + year_offset = (i - 1) // 10 # 10 posts per year + month_offset = (i - 1) % 12 + post_date = current_date - timedelta(days=365 * year_offset + 30 * month_offset) + + blog_post = BlogPage( + title=f'Test Blog Post {i}', + slug=f'test-blog-post-{i}', + date=post_date, + author=author, + excerpt=f'This is a test excerpt for blog post {i}. ' + 'It provides a brief overview of the post content.', + body=f'
This is the body content for test blog post {i}.
' + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. ' + 'Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
', + enable_comments=True, + ) + home_page.add_child(instance=blog_post) + blog_post.save_revision().publish() + + if i % 5 == 0: + self.stdout.write(f' Created {i}/{num_blog_posts} blog posts...') + + self.stdout.write(self.style.SUCCESS(f'ā Created {num_blog_posts} blog posts')) + + # Create events + self.stdout.write(self.style.NOTICE(f'\nCreating {num_events} events...')) + + event_types = ['workshop', 'webinar', 'conference', 'meetup', 'other'] + + for i in range(1, num_events + 1): + # Mix of past and future events + if i % 2 == 0: + # Future event + event_date = current_date + timedelta(days=30 * i) + else: + # Past event + event_date = current_date - timedelta(days=30 * i) + + event_type = event_types[i % len(event_types)] + + event = EventPage( + title=f'Test Event {i}', + slug=f'test-event-{i}', + date=current_date, + author=author, + start_date=event_date.date(), + end_date=(event_date + timedelta(days=1)).date() if i % 3 == 0 else event_date.date(), + location='Online' if i % 2 == 0 else 'San Francisco, CA', + event_type=event_type, + excerpt=f'This is a test excerpt for event {i}. Join us for this exciting event!', + body=f'This is the body content for test event {i}.
' + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.
', + enable_comments=True, + ) + home_page.add_child(instance=event) + event.save_revision().publish() + + if i % 5 == 0: + self.stdout.write(f' Created {i}/{num_events} events...') + + self.stdout.write(self.style.SUCCESS(f'ā Created {num_events} events')) + + # Summary + self.stdout.write(self.style.SUCCESS( + f'\nā Successfully created {num_blog_posts} blog posts and {num_events} events!' + )) + self.stdout.write(self.style.NOTICE( + '\nYou can now test:\n' + ' - Blog pagination at /blog/ (12 posts per page)\n' + ' - Events pagination at /events/ (15 events per page)\n' + ' - Year filtering on blog page\n' + )) \ No newline at end of file diff --git a/static/images/headers/pyopensci-learn-header.png b/static/images/headers/pyopensci-learn-header.png new file mode 100644 index 0000000..9c6c9a7 Binary files /dev/null and b/static/images/headers/pyopensci-learn-header.png differ diff --git a/static/images/headers/pyopensci-learn-header.webp b/static/images/headers/pyopensci-learn-header.webp new file mode 100644 index 0000000..f215fc8 Binary files /dev/null and b/static/images/headers/pyopensci-learn-header.webp differ diff --git a/tailwind.config.js b/tailwind.config.js index ceae4e6..5a25188 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -25,5 +25,7 @@ module.exports = { } }, }, - plugins: [], + plugins: [ + require('@tailwindcss/typography'), + ], } \ No newline at end of file diff --git a/templates/core/blog_index.html b/templates/core/blog_index.html index f68ad69..b1f3701 100644 --- a/templates/core/blog_index.html +++ b/templates/core/blog_index.html @@ -1,27 +1,41 @@ {% extends "base.html" %} +{% load static wagtailimages_tags %} {% block title %}{{ page_title }} - pyOpenSci{% endblock %} {% block content %} - -- {{ hero_subtitle }} -
+ ++ {{ hero_subtitle }} +
+Year filtering will be added later
+ + {% if available_years %} +- {{ hero_subtitle }} -
+ ++ {{ hero_subtitle }} +
++ pyOpenSci runs free and paid training events that teach skills that scientists need + to make their science more open and collaborative. +
pyOpenSci doesn't have any events coming up right now. However, check back to see what we are planning!
+Workshops, webinars, and community gatherings
+Browse our complete event archive
+ + + {% if available_years %} +