Add library report over all releases. (#1310)

- fixes #1288
This commit is contained in:
Brian Perrett
2024-10-04 16:01:44 -07:00
committed by GitHub
parent 274777a2e6
commit d91611b829
7 changed files with 358 additions and 117 deletions

View File

@@ -8,7 +8,7 @@ from django.urls import path, reverse
from django.utils.safestring import mark_safe
from django.shortcuts import redirect
from libraries.forms import CreateReportForm
from libraries.forms import CreateReportForm, CreateReportFullForm
from versions.tasks import import_all_library_versions
from .models import (
Category,
@@ -139,40 +139,75 @@ class LibraryAdmin(admin.ModelAdmin):
name="library_stat_detail",
),
path(
"report-form/",
self.admin_site.admin_view(self.report_form_view),
name="library_report_form",
"release-report-form/",
self.admin_site.admin_view(self.release_report_form),
name="release_report_form",
),
path(
"report/",
self.admin_site.admin_view(self.report_view),
name="library_report",
"release-report/",
self.admin_site.admin_view(self.release_report_view),
name="release_report",
),
path(
"report-full-form/",
self.admin_site.admin_view(self.report_form_full_view),
name="library_report_full_form",
),
path(
"report-full/",
self.admin_site.admin_view(self.report_full_view),
name="library_report_full",
),
]
return my_urls + urls
def report_form_view(self, request):
def release_report_form(self, request):
form = CreateReportForm()
context = {}
if request.GET.get("version", None):
if request.GET.get("submit", None):
form = CreateReportForm(request.GET)
if form.is_valid():
context.update(form.get_stats())
return redirect(
reverse("admin:library_report") + f"?{request.GET.urlencode()}"
reverse("admin:release_report") + f"?{request.GET.urlencode()}"
)
if not context:
context["form"] = form
return TemplateResponse(request, "admin/library_report_form.html", context)
def report_view(self, request):
def release_report_view(self, request):
form = CreateReportForm(request.GET)
context = {"form": form}
if form.is_valid():
context.update(form.get_stats())
else:
return redirect("admin:library_report_form")
return TemplateResponse(request, "admin/library_report_detail.html", context)
return redirect("admin:release_report_form")
return TemplateResponse(request, "admin/release_report_detail.html", context)
def report_form_full_view(self, request):
form = CreateReportFullForm()
context = {}
if request.GET.get("submit", None):
form = CreateReportFullForm(request.GET)
if form.is_valid():
context.update(form.get_stats())
return redirect(
reverse("admin:library_report_full") + f"?{request.GET.urlencode()}"
)
if not context:
context["form"] = form
return TemplateResponse(request, "admin/library_report_form.html", context)
def report_full_view(self, request):
form = CreateReportFullForm(request.GET)
context = {"form": form}
if form.is_valid():
context.update(form.get_stats())
else:
return redirect("admin:library_report_full_form")
return TemplateResponse(
request, "admin/library_report_full_detail.html", context
)
def view_stats(self, instance):
url = reverse("admin:library_stat_detail", kwargs={"pk": instance.pk})

View File

@@ -22,19 +22,14 @@ class VersionSelectionForm(Form):
)
class CreateReportForm(Form):
class CreateReportFullForm(Form):
"""Form for creating a report over all releases."""
library_queryset = Library.objects.all().order_by("name")
version = ModelChoiceField(
queryset=Version.objects.active()
.exclude(name__in=["develop", "master", "head"])
.order_by("-name")
)
library_1 = ModelChoiceField(
queryset=library_queryset,
required=False,
help_text=(
"If none are selected, the top 5 for this release will be auto-selected."
),
help_text="If none are selected, the top 5 will be auto-selected.",
)
library_2 = ModelChoiceField(
queryset=library_queryset,
@@ -65,28 +60,14 @@ class CreateReportForm(Form):
required=False,
)
def _get_top_contributors_for_version(self):
def _get_top_libraries(self):
return (
CommitAuthor.objects.filter(
commit__library_version__version=self.cleaned_data["version"]
)
.annotate(commit_count=Count("commit"))
.values("name", "avatar_url", "commit_count")
.order_by("-commit_count")[:10]
)
def _get_top_libraries_for_version(self):
return (
Library.objects.filter(
library_version=LibraryVersion.objects.filter(
library=OuterRef("id"), version=self.cleaned_data["version"]
)[:1],
)
Library.objects.all()
.annotate(commit_count=Count("library_version__commit"))
.order_by("-commit_count")[:5]
)
def _get_library_order(self, top_libraries_release):
def _get_library_order(self, top_libraries):
library_order = [
x.id
for x in [
@@ -102,7 +83,7 @@ class CreateReportForm(Form):
if x is not None
]
if not library_order:
library_order = [x.id for x in top_libraries_release]
library_order = [x.id for x in top_libraries]
return library_order
def _get_library_full_counts(self, libraries, library_order):
@@ -115,6 +96,99 @@ class CreateReportForm(Form):
key=lambda x: library_order.index(x["id"]),
)
def _get_top_contributors_overall(self):
return (
CommitAuthor.objects.all()
.annotate(commit_count=Count("commit"))
.values("name", "avatar_url", "commit_count", "github_profile_url")
.order_by("-commit_count")[:10]
)
def _get_top_contributors_for_library(self, library_order):
top_contributors_library = []
for library_id in library_order:
top_contributors_library.append(
CommitAuthor.objects.filter(
commit__library_version__library_id=library_id
)
.annotate(commit_count=Count("commit"))
.values(
"name",
"avatar_url",
"github_profile_url",
"commit_count",
"commit__library_version__library_id",
)
.order_by("-commit_count")[:10]
)
return top_contributors_library
def get_stats(self):
commit_count = Commit.objects.count()
top_libraries = self._get_top_libraries()
library_order = self._get_library_order(top_libraries)
libraries = Library.objects.filter(id__in=library_order)
library_data = [
{
"library": x[0],
"full_count": x[1],
"top_contributors": x[2],
}
for x in zip(
sorted(list(libraries), key=lambda x: library_order.index(x.id)),
self._get_library_full_counts(libraries, library_order),
self._get_top_contributors_for_library(library_order),
)
]
top_contributors = self._get_top_contributors_overall()
return {
"commit_count": commit_count,
"top_contributors": top_contributors,
"library_data": library_data,
"top_libraries": top_libraries,
"library_count": Library.objects.all().count(),
}
class CreateReportForm(CreateReportFullForm):
"""Form for creating a report for a specific release."""
version = ModelChoiceField(
queryset=Version.objects.active()
.exclude(name__in=["develop", "master", "head"])
.order_by("-name")
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields[
"library_1"
].help_text = (
"If none are selected, the top 5 for this release will be auto-selected."
)
def _get_top_contributors_for_version(self):
return (
CommitAuthor.objects.filter(
commit__library_version__version=self.cleaned_data["version"]
)
.annotate(commit_count=Count("commit"))
.values("name", "avatar_url", "commit_count", "github_profile_url")
.order_by("-commit_count")[:10]
)
def _get_top_libraries_for_version(self):
return (
Library.objects.filter(
library_version=LibraryVersion.objects.filter(
library=OuterRef("id"), version=self.cleaned_data["version"]
)[:1],
)
.annotate(commit_count=Count("library_version__commit"))
.order_by("-commit_count")[:5]
)
def _get_library_version_counts(self, libraries, library_order):
return sorted(
list(
@@ -173,6 +247,7 @@ class CreateReportForm(Form):
.values(
"name",
"avatar_url",
"github_profile_url",
"commit_count",
"commit__library_version__library_id",
)

View File

@@ -6,7 +6,8 @@
{% block object-tools-items %}
{{ block.super }}
<li><a href="{% url 'admin:update_libraries' %}" class="addlink">{% trans "Update Library Data" %}</a></li>
<li><a href="{% url 'admin:library_report_form' %}" class="addlink">{% trans "Get Release Report" %}</a></li>
<li><a href="{% url 'admin:release_report_form' %}" class="addlink">{% trans "Get Release Report" %}</a></li>
<li><a href="{% url 'admin:library_report_full_form' %}" class="addlink">{% trans "Get Library Report" %}</a></li>
{% endblock %}
</ul>
{% endblock %}

View File

@@ -0,0 +1,54 @@
{% load static humanize avatar_tags %}
<!DOCTYPE html>
<html>
<head>
<title>
{% block title %}Boost{% endblock %}
</title>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="{% block description %}{% endblock %}" />
<meta name="keywords" content="{% block keywords %}{% endblock %}" />
<meta name="author"
content="{% block author %}Boost C++ Libraries{% endblock %}" />
<link rel="shortcut icon"
href="{% static 'img/Boost_Symbol_Transparent.svg' %}"
type="image/x-icon" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100..900;1,100..900&display=swap"
rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+Mono&display=swap"
rel="stylesheet">
<link rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0" />
<link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.css" />
<!-- TODO bring this local if we like it -->
<script src="https://cdn.jsdelivr.net/npm/apexcharts"></script>
<script src="{% static 'js/boost-gecko/main.062e4862.js' %}" defer></script>
{% block extra_head %}
<link href="{% static 'css/styles.css' %}" rel="stylesheet" />
{% endblock %}
{% block css %}
<style>
@page {
margin: 0;
size: 300mm 180mm;
}
.pdf-page {
padding: 5mm;
height: 180mm;
width: 300mm;
page-break-after: always;
}
</style>
{% endblock css %}
</head>
<body>
{% block content %}
{% endblock content %}
</body>
</html>

View File

@@ -3,11 +3,11 @@
{% block content %}
{{ block.super }}
<div class='container mx-auto'>
<h1>Generate Report for Release</h1>
<h1>Generate Report</h1>
<div>
<form action="">
{{ form.as_p }}
<input class="default" type="submit" />
<input name="submit" value="Submit" class="default" type="submit" />
</form>
</div>
</div>

View File

@@ -1,70 +1,17 @@
{% load static humanize avatar_tags %}
<!DOCTYPE html>
<html>
<head>
<title>
{% block title %}Boost{% endblock %}
</title>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="{% block description %}{% endblock %}" />
<meta name="keywords" content="{% block keywords %}{% endblock %}" />
<meta name="author"
content="{% block author %}Boost C++ Libraries{% endblock %}" />
<link rel="shortcut icon"
href="{% static 'img/Boost_Symbol_Transparent.svg' %}"
type="image/x-icon" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100..900;1,100..900&display=swap"
rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+Mono&display=swap"
rel="stylesheet">
<link rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0" />
<link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.css" />
<!-- TODO bring this local if we like it -->
<script src="https://cdn.jsdelivr.net/npm/apexcharts"></script>
<script src="{% static 'js/boost-gecko/main.062e4862.js' %}" defer></script>
{% block extra_head %}
<link href="{% static 'css/styles.css' %}" rel="stylesheet" />
{% endblock %}
{% block css %}
{% endblock css %}
<style>
@page {
margin: 0;
size: 300mm 180mm;
}
.pdf-page {
padding: 5mm;
height: 180mm;
width: 300mm;
page-break-after: always;
}
</style>
</head>
{% extends "admin/library_report_base.html" %}
{% load humanize avatar_tags %}
{% block content %}
{% with bg_color='bg-gradient-to-tr from-[#7ac3e6]/50 to-[#d9b05e]/50' %}
<body>
<div>
<div class="pdf-page grid grid-cols-2 gap-x-4 items-center justify-items-center {{ bg_color }}">
<div>
<h1>Boost</h1>
<div>{{ commit_count|intcomma }} commits up through {{ version.display_name }}</div>
</div>
<div>There were {{ version_commit_count|intcomma }} commits in {{ version.display_name }}</div>
</div>
<div class="pdf-page grid grid-cols-2 gap-x-4 items-center justify-items-center {{ bg_color }}">
<div class="flex flex-col">
<h1 class="mx-auto">Boost {{ version.display_name }}</h1>
<div class="mx-auto mb-4">{{ version_commit_count|intcomma }} Commits Across {{ library_count }} Repositories</div>
<h1 class="mx-auto">Boost</h1>
<div class="mx-auto mb-4">{{ commit_count|intcomma }} in all releases across all {{ library_count }} libraries</div>
<div class="flex gap-x-2">
<div>
<div class="grid grid-cols-5 gap-2">
{% for author in top_contributors_release_overall %}
{% for author in top_contributors %}
<div class="flex flex-col gap-y-2 w-20 items-center">
{% avatar commitauthor=author %}
<div class="w-full flex flex-col">
@@ -91,20 +38,11 @@
<div>{{ item.library.description }}</div>
</div>
<div class="flex flex-col gap-y-8">
<h4>There were {{ item.version_count.commit_count }} commits in release {{ version.display_name }}</h4>
{% if item.new_contributors_count.count > 1 %}
<div>
There were {{ item.new_contributors_count.count }} new contributors this release!
</div>
{% elif item.new_contributors_count.count == 1 %}
<div>
There was {{ item.new_contributors_count.count }} new contributor this release!
</div>
{% endif %}
<h4>There are {{ item.full_count.commit_count|intcomma }} commits across all releases</h4>
<div>
<div class="mb-2">Top Contributors</div>
<div class="grid grid-cols-5 gap-2 flex-wrap">
{% for author in item.top_contributors_release %}
{% for author in item.top_contributors %}
<div class="flex flex-col gap-y-2 w-20 items-center">
{% avatar commitauthor=author %}
<div class="w-full flex flex-col justify-center items-center">
@@ -126,7 +64,7 @@
var options = {
series: [{
name: 'Commits',
data: [{% for library in top_libraries_for_version %}{{library.commit_count}}, {% endfor %}]
data: [{% for library in top_libraries %}{{library.commit_count}}, {% endfor %}]
}],
chart: {
height: 300,
@@ -154,7 +92,7 @@
}
},
xaxis: {
categories: [{% for library in top_libraries_for_version %} "{{ library.name }}", {% endfor %}],
categories: [{% for library in top_libraries %} "{{ library.name }}", {% endfor %}],
position: 'bottom',
axisBorder: {
show: false
@@ -182,6 +120,5 @@
const chart = new ApexCharts(document.querySelector("#top-committed-libraries-chart"), options);
chart.render();
</script>
</body>
{% endwith %}
</html>
{% endblock content %}

View File

@@ -0,0 +1,139 @@
{% extends "admin/library_report_base.html" %}
{% load humanize avatar_tags %}
{% block content %}
{% with bg_color='bg-gradient-to-tr from-[#7ac3e6]/50 to-[#d9b05e]/50' %}
<div>
<div class="pdf-page grid grid-cols-2 gap-x-4 items-center justify-items-center {{ bg_color }}">
<div>
<h1>Boost</h1>
<div>{{ commit_count|intcomma }} commits up through {{ version.display_name }}</div>
</div>
<div>There were {{ version_commit_count|intcomma }} commits in {{ version.display_name }}</div>
</div>
<div class="pdf-page grid grid-cols-2 gap-x-4 items-center justify-items-center {{ bg_color }}">
<div class="flex flex-col">
<h1 class="mx-auto">Boost {{ version.display_name }}</h1>
<div class="mx-auto mb-4">{{ version_commit_count|intcomma }} Commits Across {{ library_count }} Repositories</div>
<div class="flex gap-x-2">
<div>
<div class="grid grid-cols-5 gap-2">
{% for author in top_contributors_release_overall %}
<div class="flex flex-col gap-y-2 w-20 items-center">
{% avatar commitauthor=author %}
<div class="w-full flex flex-col">
<div class="text-[0.6rem] overflow-ellipsis overflow-hidden whitespace-nowrap w-full text-center">
{{ author.name }}
</div>
<div class="text-[0.6rem] mx-auto">({{ author.commit_count }})</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
<div class="flex flex-col">
<h3 class="mx-auto">Most Committed Libraries</h3>
<div id="top-committed-libraries-chart" class="w-full text-center"></div>
</div>
</div>
{% for item in library_data %}
<div class="pdf-page grid grid-cols-2 gap-x-4 items-center justify-items-center {{ bg_color }}">
<div>
<h3>{{ item.library.name }}</h3>
<div>{{ item.library.description }}</div>
</div>
<div class="flex flex-col gap-y-8">
<h4>There were {{ item.version_count.commit_count }} commits in release {{ version.display_name }}</h4>
{% if item.new_contributors_count.count > 1 %}
<div>
There were {{ item.new_contributors_count.count }} new contributors this release!
</div>
{% elif item.new_contributors_count.count == 1 %}
<div>
There was {{ item.new_contributors_count.count }} new contributor this release!
</div>
{% endif %}
<div>
<div class="mb-2">Top Contributors</div>
<div class="grid grid-cols-5 gap-2 flex-wrap">
{% for author in item.top_contributors_release %}
<div class="flex flex-col gap-y-2 w-20 items-center">
{% avatar commitauthor=author %}
<div class="w-full flex flex-col justify-center items-center">
<div class="text-[0.6rem] overflow-ellipsis overflow-hidden whitespace-nowrap w-full text-center">
{{ author.name }}
</div>
<div class="text-[0.6rem]">({{ author.commit_count }})</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
{% endfor %}
</div>
<div class="pdf-page {{ bg_color }}" style="page-break-after: avoid;">This is the last page</div>
<script>
var options = {
series: [{
name: 'Commits',
data: [{% for library in top_libraries_for_version %}{{library.commit_count}}, {% endfor %}]
}],
chart: {
height: 300,
type: 'bar',
foreColor: '#373d3f',
background: '#ffffff00',
toolbar: {
show: false,
},
},
plotOptions: {
bar: {
borderRadius: 2,
dataLabels: {
position: 'top', // top, center, bottom
},
}
},
dataLabels: {
offsetY: -16,
enabled: true,
style: {
fontSize: '11px',
colors: ["rgb(49, 74, 87)"],
}
},
xaxis: {
categories: [{% for library in top_libraries_for_version %} "{{ library.name }}", {% endfor %}],
position: 'bottom',
axisBorder: {
show: false
},
axisTicks: {
show: false
},
tooltip: {
enabled: true,
}
},
yaxis: {
axisBorder: {
show: true
},
axisTicks: {
show: true,
},
labels: {
show: true,
}
},
};
// SS: putting this in the window object, a bit hacky, to be able to access it in the light/dark switcher - probably a better way
const chart = new ApexCharts(document.querySelector("#top-committed-libraries-chart"), options);
chart.render();
</script>
{% endwith %}
{% endblock content %}