From 9057f8e05e05f32e45be9832659aaacc0efeaffa Mon Sep 17 00:00:00 2001 From: Johannes Schmidt <56628013+Jocho-Smith@users.noreply.github.com> Date: Sat, 21 Feb 2026 13:51:03 +0100 Subject: [PATCH 01/10] Update title to 'An Otter Wiki test' --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 26a7bcb2..2ace5925 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ ![](screenshot.png) -# An Otter Wiki +# An Otter Wiki test An Otter Wiki is Python-based software for collaborative content management, called a [wiki](https://en.wikipedia.org/wiki/Wiki). The From 17064279f95c03950c6f77db9bed5b16a59d6740 Mon Sep 17 00:00:00 2001 From: Johannes Schmidt Date: Sat, 21 Feb 2026 13:55:27 +0100 Subject: [PATCH 02/10] Revert "Update title to 'An Otter Wiki test'" This reverts commit 9057f8e05e05f32e45be9832659aaacc0efeaffa. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2ace5925..26a7bcb2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ ![](screenshot.png) -# An Otter Wiki test +# An Otter Wiki An Otter Wiki is Python-based software for collaborative content management, called a [wiki](https://en.wikipedia.org/wiki/Wiki). The From 19b0de059136872ac23211fe0866d39e58e25379 Mon Sep 17 00:00:00 2001 From: Johannes Schmidt Date: Sat, 21 Feb 2026 15:35:20 +0100 Subject: [PATCH 03/10] 3 commits: add admin_dashboard() that 1. builds Mermaid pie chart 2. renders the markdown to html add item *dashboard* to settings side bar add dashboard.html NOTE: Mermaid did not render by itself outside of .md files, so I hat 2 choices: either in dashboard.html or in wiki.html. Doing it in dashboard.html is ugly, but that way I ensured I understand its implications to the rest of otterwiki. adding it to wiki.html would conceptually fit better, but I doubt other system pages (user management, about, ...) will benefit from global Mermaid rendering --- otterwiki/templates/admin/dashboard.html | 18 ++++++++ otterwiki/templates/settings.html | 6 +++ otterwiki/views.py | 52 ++++++++++++++++++++++++ 3 files changed, 76 insertions(+) create mode 100644 otterwiki/templates/admin/dashboard.html diff --git a/otterwiki/templates/admin/dashboard.html b/otterwiki/templates/admin/dashboard.html new file mode 100644 index 00000000..d61f676e --- /dev/null +++ b/otterwiki/templates/admin/dashboard.html @@ -0,0 +1,18 @@ +{% extends "settings.html" %} + +{% block title %} + Contribution Dashboard + + + +{% endblock %} + +{% block content %} +
+{{htmlcontent|safe}} +
+{% endblock %} diff --git a/otterwiki/templates/settings.html b/otterwiki/templates/settings.html index d2ae5b00..884b08f5 100644 --- a/otterwiki/templates/settings.html +++ b/otterwiki/templates/settings.html @@ -58,6 +58,12 @@ Mail Preferences + + + + + Dashboard + {% endif %} {% endblock %} {% block content %} diff --git a/otterwiki/views.py b/otterwiki/views.py index bb38f962..70d569dc 100644 --- a/otterwiki/views.py +++ b/otterwiki/views.py @@ -3,6 +3,7 @@ import os import hashlib +import subprocess from flask import ( request, @@ -248,6 +249,57 @@ def admin_mail_preferences(): return otterwiki.preferences.handle_mail_preferences(request.form) +@app.route("/-/admin/dashboard") +def admin_dashboard(): + repo_path = os.path.join("app-data", "repository") + + def mermaid_commit_block(title: str, git_args: list) -> str: + """Run git log, count commits per author, return a Mermaid pie chart as markdown.""" + result = subprocess.run( + ["git", "log"] + git_args, + capture_output=True, + text=True, + cwd=repo_path, + ) + if result.returncode != 0: + raise RuntimeError(f"Git command failed ({title})") + + authors = result.stdout.splitlines() + stats = {} + for author in authors: + stats[author] = stats.get(author, 0) + 1 + sorted_stats = sorted(stats.items(), key=lambda x: x[1], reverse=True) + + lines = ["```mermaid", "pie showData", f" title {title}"] + for author, count in sorted_stats: + lines.append(f' "{author}" : {count}') + lines.append("```") + return "\n".join(lines) + + try: + # Build both blocks + all_commits_block = mermaid_commit_block( + "All Commits", ["--format=%an"] + ) + last_3_months_block = mermaid_commit_block( + "Last Month", ["--format=%an", "--since=1.months"] + ) + except RuntimeError as e: + return str(e), 500 + + # Merge both blocks + markdown_content = "\n\n".join([all_commits_block, last_3_months_block]) + + # Render Markdown + htmlcontent, _, library_requirements = render.markdown(markdown_content) + + return render_template( + "admin/dashboard.html", + htmlcontent=htmlcontent, + library_requirements=library_requirements, + ) + + @app.route( "/-/admin", methods=["POST", "GET"] ) # pyright: ignore -- false positive From 561a3a3f297c072ce90539bae812443b31e2898d Mon Sep 17 00:00:00 2001 From: Johannes Schmidt Date: Mon, 23 Feb 2026 11:42:38 +0100 Subject: [PATCH 04/10] renamed sidebar entry from Dashboard to Statistics --- otterwiki/templates/settings.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/otterwiki/templates/settings.html b/otterwiki/templates/settings.html index 884b08f5..2b2bd6c6 100644 --- a/otterwiki/templates/settings.html +++ b/otterwiki/templates/settings.html @@ -62,7 +62,7 @@ - Dashboard + Statistics {% endif %} {% endblock %} From a9e2d3e352b6b342c8d5440e158833fe08cb8b67 Mon Sep 17 00:00:00 2001 From: Johannes Schmidt Date: Mon, 23 Feb 2026 11:55:32 +0100 Subject: [PATCH 05/10] fix: remove mermaid CDN and use bundled version --- otterwiki/templates/admin/dashboard.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/otterwiki/templates/admin/dashboard.html b/otterwiki/templates/admin/dashboard.html index d61f676e..9e44de6d 100644 --- a/otterwiki/templates/admin/dashboard.html +++ b/otterwiki/templates/admin/dashboard.html @@ -2,7 +2,7 @@ {% block title %} Contribution Dashboard - + - +{% endblock %} +{% block js %} +{{ super() }} +{% set force_load_libraries = true %} +{% include 'snippets/renderer_js.html' %} {% endblock %} + {% block content %}
{{htmlcontent|safe}} From 36d19ceb9a642c1ef7a8af1a43b18d4ebeee9dd4 Mon Sep 17 00:00:00 2001 From: Johannes Schmidt Date: Mon, 23 Feb 2026 12:47:22 +0100 Subject: [PATCH 07/10] fix: replaced subproces git command with otterwiki.gitstorage + datetime --- otterwiki/views.py | 56 +++++++++++++++++++++++++--------------------- 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/otterwiki/views.py b/otterwiki/views.py index 70d569dc..1de59188 100644 --- a/otterwiki/views.py +++ b/otterwiki/views.py @@ -3,7 +3,7 @@ import os import hashlib -import subprocess +from datetime import datetime, timedelta from flask import ( request, @@ -27,6 +27,7 @@ import otterwiki.auth import otterwiki.preferences import otterwiki.tools +import otterwiki.gitstorage from otterwiki.renderer import render from otterwiki.helper import ( toast, @@ -251,46 +252,49 @@ def admin_mail_preferences(): @app.route("/-/admin/dashboard") def admin_dashboard(): - repo_path = os.path.join("app-data", "repository") - - def mermaid_commit_block(title: str, git_args: list) -> str: - """Run git log, count commits per author, return a Mermaid pie chart as markdown.""" - result = subprocess.run( - ["git", "log"] + git_args, - capture_output=True, - text=True, - cwd=repo_path, - ) - if result.returncode != 0: - raise RuntimeError(f"Git command failed ({title})") - authors = result.stdout.splitlines() + def mermaid_commit_block(title: str, since_days: int | None = None) -> str: + storage = otterwiki.gitstorage.GitStorage(app.config["REPOSITORY"]) + try: + log_entries = storage.log() + except Exception as e: + raise RuntimeError(f"Git log failed ({title}): {e}") + stats = {} - for author in authors: + + now = datetime.now().astimezone() + + for entry in log_entries: + if since_days is not None: + if entry["datetime"] < now - timedelta(days=since_days): + continue + + author = entry["author_name"] stats[author] = stats.get(author, 0) + 1 + sorted_stats = sorted(stats.items(), key=lambda x: x[1], reverse=True) - lines = ["```mermaid", "pie showData", f" title {title}"] + lines = [ + "```mermaid", + "pie showData", + f" title {title}", + ] + for author, count in sorted_stats: lines.append(f' "{author}" : {count}') + lines.append("```") + return "\n".join(lines) try: - # Build both blocks - all_commits_block = mermaid_commit_block( - "All Commits", ["--format=%an"] - ) - last_3_months_block = mermaid_commit_block( - "Last Month", ["--format=%an", "--since=1.months"] - ) + all_commits_block = mermaid_commit_block("All Commits") + last_month_block = mermaid_commit_block("Last 30 Days", since_days=30) except RuntimeError as e: return str(e), 500 - # Merge both blocks - markdown_content = "\n\n".join([all_commits_block, last_3_months_block]) + markdown_content = "\n\n".join([all_commits_block, last_month_block]) - # Render Markdown htmlcontent, _, library_requirements = render.markdown(markdown_content) return render_template( From 0e1cbadfeab367e531304dab099adc8a68b5d50f Mon Sep 17 00:00:00 2001 From: Johannes Schmidt Date: Mon, 23 Feb 2026 13:09:44 +0100 Subject: [PATCH 08/10] renamed all 'dashboard' occurrences as 'statistics' --- .../templates/admin/{dashboard.html => statistics.html} | 0 otterwiki/templates/settings.html | 2 +- otterwiki/views.py | 6 +++--- 3 files changed, 4 insertions(+), 4 deletions(-) rename otterwiki/templates/admin/{dashboard.html => statistics.html} (100%) diff --git a/otterwiki/templates/admin/dashboard.html b/otterwiki/templates/admin/statistics.html similarity index 100% rename from otterwiki/templates/admin/dashboard.html rename to otterwiki/templates/admin/statistics.html diff --git a/otterwiki/templates/settings.html b/otterwiki/templates/settings.html index 2b2bd6c6..ad581324 100644 --- a/otterwiki/templates/settings.html +++ b/otterwiki/templates/settings.html @@ -58,7 +58,7 @@ Mail Preferences - + diff --git a/otterwiki/views.py b/otterwiki/views.py index 1de59188..d2228812 100644 --- a/otterwiki/views.py +++ b/otterwiki/views.py @@ -250,8 +250,8 @@ def admin_mail_preferences(): return otterwiki.preferences.handle_mail_preferences(request.form) -@app.route("/-/admin/dashboard") -def admin_dashboard(): +@app.route("/-/admin/statistics") +def admin_statistics(): def mermaid_commit_block(title: str, since_days: int | None = None) -> str: storage = otterwiki.gitstorage.GitStorage(app.config["REPOSITORY"]) @@ -298,7 +298,7 @@ def mermaid_commit_block(title: str, since_days: int | None = None) -> str: htmlcontent, _, library_requirements = render.markdown(markdown_content) return render_template( - "admin/dashboard.html", + "admin/statistics.html", htmlcontent=htmlcontent, library_requirements=library_requirements, ) From 0db90e26d9948619b738bd6fe3a49bd7ee3bc726 Mon Sep 17 00:00:00 2001 From: Johannes Schmidt Date: Mon, 23 Feb 2026 16:15:19 +0100 Subject: [PATCH 09/10] fix: moved statistics logic from views.py to statistics.py + got rid of the markdown renderer entirely --- otterwiki/statistics.py | 38 ++++++++++++++++ otterwiki/templates/admin/statistics.html | 18 +++++++- otterwiki/views.py | 55 +++++------------------ 3 files changed, 65 insertions(+), 46 deletions(-) create mode 100644 otterwiki/statistics.py diff --git a/otterwiki/statistics.py b/otterwiki/statistics.py new file mode 100644 index 00000000..a32097e6 --- /dev/null +++ b/otterwiki/statistics.py @@ -0,0 +1,38 @@ +from datetime import datetime, timedelta +from typing import List, Tuple + +from otterwiki.gitstorage import GitStorage + + +class StatisticsService: + def __init__(self, repository_path: str): + self.storage = GitStorage(repository_path) + + def _aggregate_commits( + self, + log_entries: List[dict], + since_days: int | None = None, + ) -> List[Tuple[str, int]]: + + stats = {} + now = datetime.now().astimezone() + + for entry in log_entries: + if since_days is not None: + if entry["datetime"] < now - timedelta(days=since_days): + continue + + author = entry["author_name"] + stats[author] = stats.get(author, 0) + 1 + + return sorted(stats.items(), key=lambda x: x[1], reverse=True) + + def commit_statistics( + self, + since_days: int | None = None, + ) -> List[Tuple[str, int]]: + """ + Returns aggregated commit counts per author. + """ + log_entries = self.storage.log() + return self._aggregate_commits(log_entries, since_days) diff --git a/otterwiki/templates/admin/statistics.html b/otterwiki/templates/admin/statistics.html index c7045247..a9c9978e 100644 --- a/otterwiki/templates/admin/statistics.html +++ b/otterwiki/templates/admin/statistics.html @@ -13,6 +13,22 @@ {% block content %}
-{{htmlcontent|safe}} + +
+pie showData + title All Commits test +{% for author, count in all_commits %} + "{{ author }}" : {{ count }} +{% endfor %} +
+ +
+pie showData + title Last 30 Days +{% for author, count in last_30_days %} + "{{ author }}" : {{ count }} +{% endfor %} +
+
{% endblock %} diff --git a/otterwiki/views.py b/otterwiki/views.py index d2228812..f73987cd 100644 --- a/otterwiki/views.py +++ b/otterwiki/views.py @@ -3,7 +3,6 @@ import os import hashlib -from datetime import datetime, timedelta from flask import ( request, @@ -27,7 +26,7 @@ import otterwiki.auth import otterwiki.preferences import otterwiki.tools -import otterwiki.gitstorage +from otterwiki.statistics import StatisticsService from otterwiki.renderer import render from otterwiki.helper import ( toast, @@ -38,6 +37,7 @@ from otterwiki.util import sanitize_pagename from flask_login import login_required +from flask import current_app # @@ -251,56 +251,21 @@ def admin_mail_preferences(): @app.route("/-/admin/statistics") +@login_required def admin_statistics(): - def mermaid_commit_block(title: str, since_days: int | None = None) -> str: - storage = otterwiki.gitstorage.GitStorage(app.config["REPOSITORY"]) - try: - log_entries = storage.log() - except Exception as e: - raise RuntimeError(f"Git log failed ({title}): {e}") - - stats = {} - - now = datetime.now().astimezone() - - for entry in log_entries: - if since_days is not None: - if entry["datetime"] < now - timedelta(days=since_days): - continue - - author = entry["author_name"] - stats[author] = stats.get(author, 0) + 1 - - sorted_stats = sorted(stats.items(), key=lambda x: x[1], reverse=True) - - lines = [ - "```mermaid", - "pie showData", - f" title {title}", - ] - - for author, count in sorted_stats: - lines.append(f' "{author}" : {count}') - - lines.append("```") - - return "\n".join(lines) + service = StatisticsService(current_app.config["REPOSITORY"]) try: - all_commits_block = mermaid_commit_block("All Commits") - last_month_block = mermaid_commit_block("Last 30 Days", since_days=30) - except RuntimeError as e: - return str(e), 500 - - markdown_content = "\n\n".join([all_commits_block, last_month_block]) - - htmlcontent, _, library_requirements = render.markdown(markdown_content) + all_commits = service.commit_statistics() + last_30_days = service.commit_statistics(since_days=30) + except Exception as e: + return f"Git log failed: {e}", 500 return render_template( "admin/statistics.html", - htmlcontent=htmlcontent, - library_requirements=library_requirements, + all_commits=all_commits, + last_30_days=last_30_days, ) From 3ca9ebc759910b2f9591721d89541c4890c6ecc1 Mon Sep 17 00:00:00 2001 From: Johannes Schmidt Date: Mon, 23 Feb 2026 16:45:36 +0100 Subject: [PATCH 10/10] feature: cap the listed users at 10, so the pie chart looks cleaner with many users --- otterwiki/statistics.py | 19 +++++++++++++++++-- otterwiki/templates/admin/statistics.html | 2 +- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/otterwiki/statistics.py b/otterwiki/statistics.py index a32097e6..95710b90 100644 --- a/otterwiki/statistics.py +++ b/otterwiki/statistics.py @@ -5,8 +5,9 @@ class StatisticsService: - def __init__(self, repository_path: str): + def __init__(self, repository_path: str, top_limit: int = 10): self.storage = GitStorage(repository_path) + self.top_limit = top_limit def _aggregate_commits( self, @@ -25,7 +26,20 @@ def _aggregate_commits( author = entry["author_name"] stats[author] = stats.get(author, 0) + 1 - return sorted(stats.items(), key=lambda x: x[1], reverse=True) + sorted_stats = sorted(stats.items(), key=lambda x: x[1], reverse=True) + + # If within limit, return directly + if len(sorted_stats) <= self.top_limit: + return sorted_stats + + top_entries = sorted_stats[: self.top_limit] + remainder = sorted_stats[self.top_limit :] + + others_count = sum(count for _, count in remainder) + + top_entries.append(("Others", others_count)) + + return top_entries def commit_statistics( self, @@ -33,6 +47,7 @@ def commit_statistics( ) -> List[Tuple[str, int]]: """ Returns aggregated commit counts per author. + Only top contributors are shown; remainder grouped as 'Others'. """ log_entries = self.storage.log() return self._aggregate_commits(log_entries, since_days) diff --git a/otterwiki/templates/admin/statistics.html b/otterwiki/templates/admin/statistics.html index a9c9978e..0e528f0d 100644 --- a/otterwiki/templates/admin/statistics.html +++ b/otterwiki/templates/admin/statistics.html @@ -16,7 +16,7 @@
pie showData - title All Commits test + title All Commits {% for author, count in all_commits %} "{{ author }}" : {{ count }} {% endfor %}