Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions blog/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@

class BlogIndexPage(Page):
"""
Index page for blog posts
Index page for blog posts.

Wagtail page model for the main blog listing page.
"""
intro = RichTextField(blank=True)

Expand All @@ -20,7 +22,9 @@ class Meta:

class BlogPage(Page):
"""
Individual blog post page
Individual blog post page.

Wagtail page model for individual blog posts with date, intro, and content.
"""
date = models.DateField("Post date")
intro = models.CharField(max_length=250)
Expand Down
2 changes: 0 additions & 2 deletions core/admin.py
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
from django.contrib import admin

# Register your models here.
100 changes: 97 additions & 3 deletions core/models.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,97 @@
from django.db import models

# Create your models here.
from django.db import models
from typing import Optional


class Contributor(models.Model):
"""
Django model representing a pyOpenSci contributor.

This model mirrors the PersonModel from pyosMeta for future database migration.
Currently, contributor data is read directly from YAML files.
"""

# Basic information
name = models.CharField(max_length=255, null=True, blank=True)
github_username = models.CharField(max_length=100, unique=True)
github_image_id = models.IntegerField(null=True, blank=True)
bio = models.TextField(null=True, blank=True)
organization = models.CharField(max_length=255, null=True, blank=True)
location = models.CharField(max_length=255, null=True, blank=True)
email = models.EmailField(null=True, blank=True)

# Dates
date_added = models.DateField(null=True, blank=True)

# Role flags
deia_advisory = models.BooleanField(default=False)
editorial_board = models.BooleanField(default=False)
emeritus_editor = models.BooleanField(default=False)
advisory = models.BooleanField(default=False)
emeritus_advisory = models.BooleanField(default=False)
board = models.BooleanField(default=False)

# Social media and external links
twitter = models.CharField(max_length=50, null=True, blank=True)
mastodon = models.URLField(null=True, blank=True)
orcidid = models.CharField(max_length=50, null=True, blank=True)
website = models.URLField(null=True, blank=True)

# JSON fields for lists (SQLite compatible)
title = models.JSONField(default=list, blank=True)
partners = models.JSONField(default=list, blank=True)
contributor_type = models.JSONField(default=list, blank=True)
packages_eic = models.JSONField(default=list, blank=True)
packages_editor = models.JSONField(default=list, blank=True)
packages_submitted = models.JSONField(default=list, blank=True)
packages_reviewed = models.JSONField(default=list, blank=True)

# Metadata
sort = models.IntegerField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)

class Meta:
ordering = ['-date_added', 'sort', 'name']
verbose_name = "Contributor"
verbose_name_plural = "Contributors"

def __str__(self) -> str:
return self.display_name

@property
def display_name(self) -> str:
"""
Return name if available, otherwise GitHub username.

Returns
-------
str
The contributor's display name.
"""
return self.name or f"@{self.github_username}"

@property
def github_avatar_url(self) -> Optional[str]:
"""
Generate GitHub avatar URL from image ID.

Returns
-------
str or None
GitHub avatar URL if image ID exists, None otherwise.
"""
if self.github_image_id:
return f"https://avatars.githubusercontent.com/u/{self.github_image_id}?s=400&v=4"
return None

@property
def github_profile_url(self) -> str:
"""
Generate GitHub profile URL.

Returns
-------
str
GitHub profile URL for the contributor.
"""
return f"https://github.com/{self.github_username}"
51 changes: 34 additions & 17 deletions core/templatetags/image_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,21 @@ def responsive_image(image_path, alt_text="", css_classes="", sizes="100vw"):
"""
Generate a responsive picture element with WebP and PNG fallback.

Args:
image_path: Path to the image without extension (e.g., 'headers/pyopensci-sprints')
alt_text: Alt text for accessibility
css_classes: CSS classes to apply to the img element
sizes: Sizes attribute for responsive images
Parameters
----------
image_path : str
Path to the image without extension (e.g., 'headers/pyopensci-sprints').
alt_text : str, default ""
Alt text for accessibility.
css_classes : str, default ""
CSS classes to apply to the img element.
sizes : str, default "100vw"
Sizes attribute for responsive images.

Returns:
HTML picture element with WebP and PNG fallback
Returns
-------
str
HTML picture element with WebP and PNG fallback.
"""
webp_path = static(f"images/{image_path}.webp")
png_path = static(f"images/{image_path}.png")
Expand All @@ -37,12 +44,17 @@ def hero_image(image_path, alt_text=""):
"""
Generate a hero image with WebP support and optimized loading.

Args:
image_path: Path to the image without extension
alt_text: Alt text for accessibility
Parameters
----------
image_path : str
Path to the image without extension.
alt_text : str, default ""
Alt text for accessibility.

Returns:
HTML picture element optimized for hero sections
Returns
-------
str
HTML picture element optimized for hero sections.
"""
return responsive_image(
image_path=image_path,
Expand All @@ -57,12 +69,17 @@ def card_image(image_path, alt_text=""):
"""
Generate a card image with WebP support.

Args:
image_path: Path to the image without extension
alt_text: Alt text for accessibility
Parameters
----------
image_path : str
Path to the image without extension.
alt_text : str, default ""
Alt text for accessibility.

Returns:
HTML picture element optimized for card layouts
Returns
-------
str
HTML picture element optimized for card layouts.
"""
return responsive_image(
image_path=image_path,
Expand Down
131 changes: 131 additions & 0 deletions core/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
"""
Utility functions for working with contributor data.

This module provides functions to fetch and parse contributor data from YAML files,
following the same format used by the Jekyll site and pyosMeta package.
"""

from ruamel.yaml import YAML, YAMLError
import logging
from typing import List, Dict, Any
from urllib.request import urlopen
from urllib.error import URLError

logger = logging.getLogger(__name__)

# Initialize YAML parser with safe loading
yaml = YAML(typ='safe')


class ContributorDataError(Exception):
"""Custom exception for contributor data related errors."""
pass


def fetch_contributors_yaml(url: str = None) -> List[Dict[str, Any]]:
"""
Fetch contributor data from YAML source.

Parameters
----------
url : str, optional
URL to fetch YAML from. If None, uses the default pyOpenSci GitHub URL.

Returns
-------
list of dict
List of contributor dictionaries.

Raises
------
ContributorDataError
If data cannot be fetched or parsed.
"""
if url is None:
url = "https://raw.githubusercontent.com/pyOpenSci/pyopensci.github.io/main/_data/contributors.yml"

try:
with urlopen(url) as response:
yaml_content = response.read().decode('utf-8')
contributors = yaml.load(yaml_content)

if not isinstance(contributors, list):
raise ContributorDataError("YAML data should be a list of contributors")

logger.info(f"Successfully fetched {len(contributors)} contributors from {url}")
return contributors

except URLError as e:
logger.error(f"Failed to fetch contributors from {url}: {e}")
raise ContributorDataError(f"Network error: {e}")
except YAMLError as e:
logger.error(f"Failed to parse YAML: {e}")
raise ContributorDataError(f"YAML parsing error: {e}")
except Exception as e:
logger.error(f"Unexpected error fetching contributors: {e}")
raise ContributorDataError(f"Unexpected error: {e}")



def get_recent_contributors(count: int = 4) -> List[Dict[str, Any]]:
"""
Get the most recent contributors.

Parameters
----------
count : int, default 4
Number of recent contributors to return.

Returns
-------
list of dict
List of recent contributor dictionaries, sorted by date_added descending.
"""
try:
contributors = fetch_contributors_yaml()

# Return most recent contributors (last items in list)
reversed_contributors = list(reversed(contributors))

return reversed_contributors[:count]

except ContributorDataError as e:
logger.error(f"Failed to get recent contributors: {e}")
return []
except Exception as e:
logger.error(f"Unexpected error getting recent contributors: {e}")
return []


def generate_github_avatar_url(github_image_id: int) -> str:
"""
Generate GitHub avatar URL from image ID.

Parameters
----------
github_image_id : int
GitHub user's image ID.

Returns
-------
str
GitHub avatar URL.
"""
return f"https://avatars.githubusercontent.com/u/{github_image_id}?s=400&v=4"


def generate_github_profile_url(github_username: str) -> str:
"""
Generate GitHub profile URL from username.

Parameters
----------
github_username : str
GitHub username.

Returns
-------
str
GitHub profile URL.
"""
return f"https://github.com/{github_username}"
Loading