Skip to content
Open
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
2 changes: 2 additions & 0 deletions netlify.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
[build]
publish = "build"
command = """
curl -fsSL https://github.com/quarto-dev/quarto-cli/releases/download/v1.5.57/quarto-1.5.57-linux-amd64.tar.gz | tar -xz -C /tmp &&
export PATH="/tmp/quarto-1.5.57/bin:$PATH" &&
python -m pip install --upgrade pip &&
python -m pip install poetry &&
poetry check &&
Expand Down
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ ignore = [
"F811",
"PLR0911", # Too many return statements
"PLR0912", # Too many branches
"PLR0913",
"PLR0913", # Too many arguments
"PLR0917", # Too many positional arguments
"PLC0415", # Import outside top level
]
select = [
"E", # pycodestyle
Expand Down
4 changes: 1 addition & 3 deletions scripts/check-broken-links-internal.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,7 @@ def check_links(folder_path: Path, port: int):
folder_path = Path(args.folder)
HTTP_PORT = args.port

if not folder_path.exists():
print(f"Error: The path {folder_path} doesn't exist.")
sys.exit(1)
folder_path.mkdir(parents=True, exist_ok=True)

# Start the HTTP server
server = start_http_server(folder_path, HTTP_PORT)
Expand Down
84 changes: 63 additions & 21 deletions scripts/check-broken-links-md.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,59 @@
"""Check if ther eis any broken links."""
"""Check if there is any broken links."""

from __future__ import annotations

import os
import subprocess
import sys

# List of exception URLs
# List of exception URLs (known to block crawlers/bots)
exception_urls = [
"https://www.linkedin.com/",
"https://twitter.com/",
"https://x.com/",
]

MIN_HTTP_SUCCESS = 200
MAX_HTTP_REDIRECT = 400

# HTTP status codes for bot-blocking, rate limiting, or server issues
ignored_status_codes = {
400,
401,
403,
405,
406,
429,
500,
502,
503,
504,
999,
}


def is_error(line: str) -> bool:
"""Determine if a linkcheckmd log line represents a real broken link."""
line = line.strip()
if not line.startswith("("):
return False

if any(exc in line for exc in exception_urls):
return False

# Try extracting the status code from the end of the tuple
try:
code_str = line.rstrip(")").rsplit(",", 1)[-1].strip()
code = int(code_str)
# 2xx (Success) and 3xx (Redirection) are valid HTTP responses
if MIN_HTTP_SUCCESS <= code < MAX_HTTP_REDIRECT:
return False
if code in ignored_status_codes:
return False
except ValueError:
pass

return True


def process_log() -> None:
"""Run the command and capture the output."""
Expand All @@ -18,46 +62,44 @@ def process_log() -> None:

try:
subprocess.run(
["python", "-m", "linkcheckmd", "-r", "-v", "-m", "get", "pages"],
[
sys.executable,
"-m",
"linkcheckmd",
"-r",
"-v",
"-m",
"get",
"pages",
],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
check=True,
)
except subprocess.CalledProcessError as e:
exitcode = e.returncode
# for some reason they were swapped
log_err = e.stdout
log_err = (e.stdout or "") + "\n" + (e.stderr or "")

if exitcode == 0:
print("[II] All links are ok.")
return

flagged_errors = []
for line in log_err.splitlines():
line = line.strip() # noqa: PLW2901
# Check if the line starts with '('
if not line.startswith("("):
continue

if line.endswith("429)"):
# Too Many Requests http error
continue
# Extract the URL using regex
for exception_url in exception_urls:
if exception_url not in line:
flagged_errors.append(line)
if is_error(line):
flagged_errors.append(line.strip())

# Print flagged errors
if not flagged_errors:
print("[II] All links are ok.")
print("No errors flagged. All URLs are in the exception list.")
return

print("Errors flagged for the following URLs:")
print("Errors flagged for the following URLs:", flush=True)
for line in flagged_errors:
print(line)
os._exit(1)
print(line, flush=True)
sys.exit(1)


# Run the script
Expand Down
4 changes: 2 additions & 2 deletions tests/test_analytics_dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,7 @@ def test_accessible_controls_and_tables_match_fixture(self):
):
self.assertEqual(row.select_one("th").get_text(), data["date"])
self.assertEqual(
row.select_one("td").get_text(), f'{data["pageviews"]:,}'
row.select_one("td").get_text(), f"{data['pageviews']:,}"
)
view = presentation.dashboard(fixture)
for panel in view["panels"]:
Expand All @@ -350,7 +350,7 @@ def test_accessible_controls_and_tables_match_fixture(self):
)
self.assertEqual(
[node.get_text() for node in row.select("td")],
[f'{data["value"]:,}', data["share_label"]],
[f"{data['value']:,}", data["share_label"]],
)
self.assertAlmostEqual(
sum(row["share"] for row in view["panels"][0]["display_rows"]), 100
Expand Down
4 changes: 2 additions & 2 deletions tests/test_analytics_presentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,10 @@ def test_report_table_and_cards_match_json(self):
self.assertEqual(
node.select_one('th[scope="row"]').get_text(), item["month"]
)
self.assertIn(f'{item["pageviews"]:,}', node.get_text())
self.assertIn(f"{item['pageviews']:,}", node.get_text())
self.assertEqual(
node.select_one("td").get_text(),
f'{item["start"]} \N{EN DASH} {item["end"]}',
f"{item['start']} \N{EN DASH} {item['end']}",
)
self.assertIsNotNone(page.select_one("table caption"))
self.assertEqual(
Expand Down
100 changes: 100 additions & 0 deletions tests/test_social_icons.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
"""Test social icons including Bluesky icon in templates and pages."""

from __future__ import annotations

import unittest

from pathlib import Path

from bs4 import BeautifulSoup


class TestSocialIcons(unittest.TestCase):
"""Test social icons definition and usage."""

def setUp(self) -> None:
"""Set up paths."""
self.root = Path(__file__).resolve().parents[1]
self.theme_dir = self.root / "theme"
self.sprites_file = self.theme_dir / "icons" / "sprites.svg"
self.partners_template = self.theme_dir / "partners.html"
self.base_template = self.theme_dir / "base.html"

def test_bluesky_symbol_in_sprites(self) -> None:
"""Verify bluesky symbol is defined in sprites.svg."""
self.assertTrue(self.sprites_file.exists())
content = self.sprites_file.read_text(encoding="utf-8")

soup = BeautifulSoup(content, "html.parser")
symbol = soup.find("symbol", id="bluesky")
self.assertIsNotNone(
symbol, "symbol with id='bluesky' must be present in sprites.svg"
)
self.assertEqual(symbol.get("viewbox"), "0 0 24 24")

path = symbol.find("path")
self.assertIsNotNone(path, "bluesky symbol must contain a path")
self.assertTrue(
len(path.get("d", "")) > 0,
"bluesky path 'd' attribute must not be empty",
)

def test_sprites_included_in_base_template(self) -> None:
"""Verify sprites.svg is included in theme/base.html."""
content = self.base_template.read_text(encoding="utf-8")
self.assertIn('{% include "icons/sprites.svg" %}', content)

def test_partners_template_renders_bluesky(self) -> None:
"""Verify theme/partners.html includes bluesky icon link."""
content = self.partners_template.read_text(encoding="utf-8")
self.assertIn("'bluesky' in partner", content)
self.assertIn('xlink:href="#bluesky"', content)

def test_rendered_partners_page(self) -> None:
"""Verify rendered partners HTML has bluesky symbol and links."""
partners_html = (
self.root / "build" / "partnership" / "partners" / "index.html"
)
if not partners_html.exists():
return

soup = BeautifulSoup(
partners_html.read_text(encoding="utf-8"), "html.parser"
)

# 1. bluesky symbol must be present in the document
bluesky_symbol = soup.find("symbol", id="bluesky")
self.assertIsNotNone(
bluesky_symbol,
"Rendered partners page must contain <symbol id='bluesky'> in DOM",
)

# 2. Check pyOpenSci partner card has bluesky link
pyopensci_bsky = soup.find(
"a", href="https://bsky.app/profile/pyopensci.bsky.social"
)
self.assertIsNotNone(
pyopensci_bsky, "pyOpenSci Bluesky link must be present"
)
self.assertEqual(pyopensci_bsky.get("aria-label"), "Bluesky")

use_tag = pyopensci_bsky.find("use")
self.assertIsNotNone(
use_tag, "Bluesky link must contain <use> SVG tag"
)
href = use_tag.get("xlink:href") or use_tag.get("href")
self.assertEqual(href, "#bluesky")

# 3. All <use> references should have matching <symbol> or element id
all_symbols = {
s.get("id") for s in soup.find_all("symbol") if s.get("id")
}
for use in soup.find_all("use"):
target = use.get("xlink:href") or use.get("href")
if target and target.startswith("#"):
target_id = target[1:]
self.assertTrue(
target_id in all_symbols
or soup.find(id=target_id) is not None,
f"Referenced symbol #{target_id} not found in DOM",
)
2 changes: 0 additions & 2 deletions theme/icons/sprites.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading