diff --git a/finbot/apps/cc/routes/challenges.py b/finbot/apps/cc/routes/challenges.py index 2b44467b..3cd6034b 100644 --- a/finbot/apps/cc/routes/challenges.py +++ b/finbot/apps/cc/routes/challenges.py @@ -37,7 +37,11 @@ def _challenge_list_with_stats(db) -> list[dict]: total_attempts = sum(p.attempts for p in progress_rows) hints_used = sum(p.hints_used for p in progress_rows) - completed_rows = [p for p in progress_rows if p.status == "completed" and p.completion_time_seconds] + completed_rows = [ + p + for p in progress_rows + if p.status == "completed" and p.completion_time_seconds is not None + ] avg_solve = ( int(sum(p.completion_time_seconds for p in completed_rows) / len(completed_rows)) if completed_rows else None diff --git a/finbot/apps/cc/templates/pages/challenges.html b/finbot/apps/cc/templates/pages/challenges.html index 60a819cb..5d4f595f 100644 --- a/finbot/apps/cc/templates/pages/challenges.html +++ b/finbot/apps/cc/templates/pages/challenges.html @@ -100,7 +100,7 @@ {% endif %} - {% if c.avg_solve_seconds %} + {% if c.avg_solve_seconds is not none %} {{ (c.avg_solve_seconds // 60) }}m {% else %} - diff --git a/tests/unit/apps/cc/test_challenges.py b/tests/unit/apps/cc/test_challenges.py new file mode 100644 index 00000000..08ed30e9 --- /dev/null +++ b/tests/unit/apps/cc/test_challenges.py @@ -0,0 +1,83 @@ +import re +from pathlib import Path +from types import SimpleNamespace +from unittest.mock import MagicMock + +from jinja2 import Environment, FileSystemLoader + +from finbot.apps.cc.routes.challenges import _challenge_list_with_stats + + +def _challenge_stats(*completion_times: int | None) -> dict: + challenge = SimpleNamespace( + category="test", + description="", + detector_class="MissingDetector", + difficulty="beginner", + hints=None, + id="test-challenge", + is_active=True, + labels=None, + points=100, + prerequisites=None, + subcategory=None, + title="Test Challenge", + ) + progress_rows = [ + SimpleNamespace( + attempts=1, + completion_time_seconds=completion_time, + hints_used=0, + status="completed", + user_id=f"user-{index}", + ) + for index, completion_time in enumerate(completion_times) + ] + challenge_query = MagicMock() + challenge_query.order_by.return_value.all.return_value = [challenge] + progress_query = MagicMock() + progress_query.filter.return_value.all.return_value = progress_rows + db = MagicMock() + db.query.side_effect = [challenge_query, progress_query] + + return _challenge_list_with_stats(db)[0] + + +def test_average_solve_time_includes_zero_and_excludes_missing_values() -> None: + stats = _challenge_stats(0, 10, None) + + assert stats["completions"] == 3 + assert stats["avg_solve_seconds"] == 5 + + +def test_zero_second_average_is_preserved() -> None: + stats = _challenge_stats(0) + + assert stats["avg_solve_seconds"] == 0 + + +def test_challenge_template_renders_zero_second_average() -> None: + template_dir = Path(__file__).parents[4] / "finbot" / "apps" / "cc" / "templates" + template = Environment(loader=FileSystemLoader(template_dir)).get_template( + "pages/challenges.html" + ) + stats = _challenge_stats(0) + + rendered = template.render( + categories=["test"], + challenges=[stats], + coverage=[], + difficulties=["beginner"], + summary={ + "active": 1, + "inactive": 0, + "invalid_detectors": 1, + "solved": 1, + "total": 1, + "unsolved": 0, + }, + url_for=lambda *args, **kwargs: "/static", + user=None, + ) + + assert re.search(r">\s*0m\s*<", rendered)