Skip to content
53 changes: 53 additions & 0 deletions otterwiki/statistics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from datetime import datetime, timedelta
from typing import List, Tuple

from otterwiki.gitstorage import GitStorage


class StatisticsService:
def __init__(self, repository_path: str, top_limit: int = 10):
self.storage = GitStorage(repository_path)
self.top_limit = top_limit

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

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,
since_days: int | None = None,
) -> 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)
34 changes: 34 additions & 0 deletions otterwiki/templates/admin/statistics.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{% extends "settings.html" %}

{% block title %}
<title>Contribution Dashboard</title>
{% endblock %}

{% block js %}
{{ super() }}
{% set force_load_libraries = true %}
{% include 'snippets/renderer_js.html' %}
{% endblock %}


{% block content %}
<div class="page">

<div class="mermaid">
pie showData
title All Commits
{% for author, count in all_commits %}
"{{ author }}" : {{ count }}
{% endfor %}
</div>

<div class="mermaid">
pie showData
title Last 30 Days
{% for author, count in last_30_days %}
"{{ author }}" : {{ count }}
{% endfor %}
</div>

</div>
{% endblock %}
6 changes: 6 additions & 0 deletions otterwiki/templates/settings.html
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@
</span>
Mail Preferences
</a>
<a href="{{ url_for("admin_statistics") }}" class="sidebar-link sidebar-link-with-icon">
<span class="sidebar-icon" style="min-width: 3rem;">
<i class="fas fa-chart-pie"></i>
</span>
Statistics
</a>
{% endif %}
{% endblock %}
{% block content %}
Expand Down
21 changes: 21 additions & 0 deletions otterwiki/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import otterwiki.auth
import otterwiki.preferences
import otterwiki.tools
from otterwiki.statistics import StatisticsService
from otterwiki.renderer import render
from otterwiki.helper import (
toast,
Expand All @@ -36,6 +37,7 @@
from otterwiki.util import sanitize_pagename

from flask_login import login_required
from flask import current_app


#
Expand Down Expand Up @@ -248,6 +250,25 @@ def admin_mail_preferences():
return otterwiki.preferences.handle_mail_preferences(request.form)


@app.route("/-/admin/statistics")
@login_required
def admin_statistics():

service = StatisticsService(current_app.config["REPOSITORY"])

try:
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",
all_commits=all_commits,
last_30_days=last_30_days,
)


@app.route(
"/-/admin", methods=["POST", "GET"]
) # pyright: ignore -- false positive
Expand Down