Decouple release reports from releases (#1737)

This commit is contained in:
daveoconnor
2025-07-08 16:09:01 -07:00
committed by GitHub
parent 0b146ce199
commit 301aaeac31
11 changed files with 308 additions and 147 deletions

View File

@@ -334,7 +334,7 @@ For this to work `SLACK_BOT_API` must be set in the `.env` file.
| Options | Format | Description |
|----------------|--------|----------------------------------------------------------------------------------------------------------------------|
| `--start_date` | date | If passed, retrieves data from the start date supplied, d-m-y, default 20-11-1998 (the start of the data in mailman) |
| `--start_date` | date | If passed, retrieves data from the start date supplied, d-m-y, default 1998-11-20 (the start of the data in mailman) |
| `--end_date` | date | If passed, If passed, retrieves data until the start date supplied, d-m-y, default today |
## `link_contributors_to_users`

View File

@@ -16,7 +16,7 @@ from reports.generation import (
get_new_subscribers_stats,
)
from slack.models import Channel, SlackActivityBucket, SlackUser
from versions.models import Version
from versions.models import Version, ReportConfiguration
from .models import (
Commit,
CommitAuthor,
@@ -232,8 +232,8 @@ class CreateReportForm(CreateReportFullForm):
html_template_name = "admin/release_report_detail.html"
version = ModelChoiceField(
queryset=Version.objects.minor_versions().order_by("-version_array")
report_configuration = ModelChoiceField(
queryset=ReportConfiguration.objects.order_by("-version")
)
def __init__(self, *args, **kwargs):
@@ -255,14 +255,12 @@ class CreateReportForm(CreateReportFullForm):
self.cleaned_data["library_8"],
]
lib_string = ",".join(str(x.id) if x else "" for x in chosen_libraries)
version = self.cleaned_data["version"]
return f"release-report-{lib_string}-{version.name}"
report_configuration = self.cleaned_data["report_configuration"]
return f"release-report-{lib_string}-{report_configuration.version}"
def _get_top_contributors_for_version(self):
def _get_top_contributors_for_version(self, version):
return (
CommitAuthor.objects.filter(
commit__library_version__version=self.cleaned_data["version"]
)
CommitAuthor.objects.filter(commit__library_version__version=version)
.annotate(
commit_count=Count(
"commit",
@@ -277,30 +275,32 @@ class CreateReportForm(CreateReportFullForm):
def _get_library_queryset_by_version(
self, version: Version, annotate_commit_count=False
):
qs = self.library_queryset.filter(
library_version=LibraryVersion.objects.filter(
library=OuterRef("id"), version=version
)[:1],
)
qs = self.library_queryset.none()
if version:
qs = self.library_queryset.filter(
library_version=LibraryVersion.objects.filter(
library=OuterRef("id"), version=version
)[:1],
)
if annotate_commit_count:
qs = qs.annotate(commit_count=Count("library_version__commit"))
return qs
def _get_top_libraries_for_version(self):
def _get_top_libraries_for_version(self, version):
library_qs = self._get_library_queryset_by_version(
self.cleaned_data["version"], annotate_commit_count=True
version, annotate_commit_count=True
)
return library_qs.order_by("-commit_count")
def _get_libraries_by_name(self):
def _get_libraries_by_name(self, version):
library_qs = self._get_library_queryset_by_version(
self.cleaned_data["version"], annotate_commit_count=True
version, annotate_commit_count=True
)
return library_qs.order_by("name")
def _get_libraries_by_quality(self):
def _get_libraries_by_quality(self, version):
# returns "great", "good", and "standard" libraries in that order
library_qs = self._get_library_queryset_by_version(self.cleaned_data["version"])
library_qs = self._get_library_queryset_by_version(version)
return list(
chain(
library_qs.filter(graphic__isnull=False),
@@ -309,17 +309,16 @@ class CreateReportForm(CreateReportFullForm):
)
)
def _get_library_version_counts(self, libraries, library_order):
def _get_library_version_counts(self, library_order, version):
library_qs = self._get_library_queryset_by_version(
self.cleaned_data["version"], annotate_commit_count=True
version, annotate_commit_count=True
)
return sorted(
list(library_qs.values("commit_count", "id")),
key=lambda x: library_order.index(x["id"]),
)
def _global_new_contributors(self, library_version):
version = self.cleaned_data["version"]
def _global_new_contributors(self, version):
version_lt = list(
Version.objects.minor_versions()
.filter(version_array__lt=version.cleaned_version_parts_int)
@@ -343,8 +342,7 @@ class CreateReportForm(CreateReportFullForm):
return set(version_author_ids) - set(prior_version_author_ids)
def _count_new_contributors(self, libraries, library_order):
version = self.cleaned_data["version"]
def _count_new_contributors(self, libraries, library_order, version):
version_lt = list(
Version.objects.minor_versions()
.filter(version_array__lt=version.cleaned_version_parts_int)
@@ -382,12 +380,12 @@ class CreateReportForm(CreateReportFullForm):
key=lambda x: library_order.index(x["id"]),
)
def _count_issues(self, libraries, library_order, version):
def _count_issues(self, libraries, library_order, version, prior_version):
data = {
x["library_id"]: x
for x in Issue.objects.count_opened_closed_during_release(version).filter(
library_id__in=[x.id for x in libraries]
)
for x in Issue.objects.count_opened_closed_during_release(
version, prior_version
).filter(library_id__in=[x.id for x in libraries])
}
ret = []
for lib_id in library_order:
@@ -397,14 +395,14 @@ class CreateReportForm(CreateReportFullForm):
ret.append({"opened": 0, "closed": 0, "library_id": lib_id})
return ret
def _count_commit_contributors_totals(self, version):
def _count_commit_contributors_totals(self, version, prior_version):
"""Get a count of contributors for this release, and a count of
new contributors.
"""
version_lt = list(
Version.objects.minor_versions()
.filter(version_array__lt=version.cleaned_version_parts_int)
.filter(version_array__lte=prior_version.cleaned_version_parts_int)
.values_list("id", flat=True)
)
version_lte = version_lt + [version.id]
@@ -439,13 +437,13 @@ class CreateReportForm(CreateReportFullForm):
this_release_count = qs["this_release_count"]
return this_release_count, new_count
def _get_top_contributors_for_library_version(self, library_order):
def _get_top_contributors_for_library_version(self, library_order, version):
top_contributors_release = []
for library_id in library_order:
top_contributors_release.append(
CommitAuthor.objects.filter(
commit__library_version=LibraryVersion.objects.get(
version=self.cleaned_data["version"], library_id=library_id
version=version, library_id=library_id
)
)
.annotate(commit_count=Count("commit"))
@@ -453,10 +451,10 @@ class CreateReportForm(CreateReportFullForm):
)
return top_contributors_release
def _count_mailinglist_contributors(self, version):
def _count_mailinglist_contributors(self, version, prior_version):
version_lt = list(
Version.objects.minor_versions()
.filter(version_array__lt=version.cleaned_version_parts_int)
.filter(version_array__lte=prior_version.cleaned_version_parts_int)
.values_list("id", flat=True)
)
version_lte = version_lt + [version.id]
@@ -620,7 +618,9 @@ class CreateReportForm(CreateReportFullForm):
):
"""Get slack stats for specific channels, or all channels."""
start = prior_version.release_date
end = version.release_date - timedelta(days=1)
end = date.today()
if version.release_date:
end = version.release_date - timedelta(days=1)
# count of all messages in the date range
q = Q(day__range=[start, end])
if channels:
@@ -671,7 +671,15 @@ class CreateReportForm(CreateReportFullForm):
return diffs
def get_stats(self):
version = self.cleaned_data["version"]
report_configuration = self.cleaned_data["report_configuration"]
version = Version.objects.filter(name=report_configuration.version).first()
prior_version = None
if not version:
# if the version is not set then the user has chosen a report configuration
# that's not matching a live version, so we use the most recent version
version = Version.objects.filter(name="master").first()
prior_version = Version.objects.most_recent()
downloads = {
k: list(v)
@@ -680,12 +688,14 @@ class CreateReportForm(CreateReportFullForm):
key=attrgetter("operating_system"),
)
}
prior_version = (
Version.objects.minor_versions()
.filter(version_array__lt=version.cleaned_version_parts_int)
.order_by("-version_array")
.first()
)
if not prior_version:
prior_version = (
Version.objects.minor_versions()
.filter(version_array__lt=version.cleaned_version_parts_int)
.order_by("-version_array")
.first()
)
commit_count = Commit.objects.filter(
library_version__version__name__lte=version.name,
@@ -696,8 +706,8 @@ class CreateReportForm(CreateReportFullForm):
library_version__library__in=self.library_queryset,
).count()
top_libraries_for_version = self._get_top_libraries_for_version()
top_libraries_by_name = self._get_libraries_by_name()
top_libraries_for_version = self._get_top_libraries_for_version(version)
top_libraries_by_name = self._get_libraries_by_name(version)
library_order = self._get_library_order(top_libraries_by_name)
libraries = Library.objects.filter(id__in=library_order).order_by(
Case(
@@ -719,10 +729,10 @@ class CreateReportForm(CreateReportFullForm):
for item in zip(
libraries,
self._get_library_full_counts(libraries, library_order),
self._get_library_version_counts(libraries, library_order),
self._get_top_contributors_for_library_version(library_order),
self._count_new_contributors(libraries, library_order),
self._count_issues(libraries, library_order, version),
self._get_library_version_counts(library_order, version),
self._get_top_contributors_for_library_version(library_order, version),
self._count_new_contributors(libraries, library_order, version),
self._count_issues(libraries, library_order, version, prior_version),
self._get_library_versions(library_order, version),
self._get_dependency_data(library_order, version),
)
@@ -730,7 +740,7 @@ class CreateReportForm(CreateReportFullForm):
library_data = [
x for x in library_data if x["version_count"]["commit_count"] > 0
]
top_contributors = self._get_top_contributors_for_version()
top_contributors = self._get_top_contributors_for_version(version)
# total messages sent during this release (version)
total_mailinglist_count = EmailData.objects.filter(version=version).aggregate(
total=Sum("count")
@@ -743,11 +753,11 @@ class CreateReportForm(CreateReportFullForm):
(
mailinglist_contributor_release_count,
mailinglist_contributor_new_count,
) = self._count_mailinglist_contributors(version)
) = self._count_mailinglist_contributors(version, prior_version)
(
commit_contributors_release_count,
commit_contributors_new_count,
) = self._count_commit_contributors_totals(version)
) = self._count_commit_contributors_totals(version, prior_version)
library_count = LibraryVersion.objects.filter(
version=version,
library__in=self.library_queryset,
@@ -775,22 +785,35 @@ class CreateReportForm(CreateReportFullForm):
slack_channels = batched(
Channel.objects.filter(name__istartswith="boost").order_by("name"), 10
)
committee_members = version.financial_committee_members.all()
committee_members = report_configuration.financial_committee_members.all()
mailinglist_post_stats = get_mailing_list_post_stats(
prior_version.release_date, version.release_date
prior_version.release_date, version.release_date or date.today()
)
new_subscribers_stats = get_new_subscribers_stats(
prior_version.release_date, version.release_date
prior_version.release_date, version.release_date or date.today()
)
library_index_library_data = []
for library in self._get_libraries_by_quality():
for library in self._get_libraries_by_quality(version):
library_index_library_data.append(
(
library,
library in [lib["library"] for lib in library_data],
)
)
wordcloud_base64, wordcloud_top_words = generate_wordcloud(version)
wordcloud_base64, wordcloud_top_words = generate_wordcloud(
version, prior_version
)
opened_issues_count = (
Issue.objects.filter(library__in=self.library_queryset)
.opened_during_release(version, prior_version)
.count()
)
closed_issues_count = (
Issue.objects.filter(library__in=self.library_queryset)
.closed_during_release(version, prior_version)
.count()
)
return {
"committee_members": committee_members,
@@ -799,17 +822,10 @@ class CreateReportForm(CreateReportFullForm):
"wordcloud_base64": wordcloud_base64,
"wordcloud_frequencies": wordcloud_top_words,
"version": version,
"report_configuration": report_configuration,
"prior_version": prior_version,
"opened_issues_count": Issue.objects.filter(
library__in=self.library_queryset
)
.opened_during_release(version)
.count(),
"closed_issues_count": Issue.objects.filter(
library__in=self.library_queryset
)
.closed_during_release(version)
.count(),
"opened_issues_count": opened_issues_count,
"closed_issues_count": closed_issues_count,
"mailinglist_counts": mailinglist_counts,
"mailinglist_total": total_mailinglist_count or 0,
"mailinglist_contributor_release_count": mailinglist_contributor_release_count, # noqa: E501

View File

@@ -111,11 +111,15 @@ def get_commit_data_for_repo_versions(key, min_version=""):
if not is_clone_successful:
logger.error(f"Clone failed for {library.key}. {message=} {error=}")
return
versions = [""] + list(
Version.objects.minor_versions()
.filter(library_version__library__key=library.key)
.order_by("version_array")
.values_list("name", flat=True)
versions = (
[""]
+ list(
Version.objects.minor_versions()
.filter(library_version__library__key=library.key)
.order_by("version_array")
.values_list("name", flat=True)
)
+ ["master"]
)
for a, b in zip(versions, versions[1:]):
if a < min_version and b < min_version:
@@ -489,17 +493,29 @@ class LibraryUpdater:
)
CommitAuthorEmail.objects.create(email=commit.email, author=author)
authors[commit.email] = author
return Commit(
author=author,
library_version=library_versions[commit.version],
sha=commit.sha,
message=commit.message,
committed_at=commit.committed_at,
is_merge=commit.is_merge,
)
try:
library_version = library_versions[commit.version]
return Commit(
author=author,
library_version=library_version,
sha=commit.sha,
message=commit.message,
committed_at=commit.committed_at,
is_merge=commit.is_merge,
)
except KeyError:
logger.error(f"KeyError {commit.version=}")
return None
def handle_version_diff_stat(diff: VersionDiffStat):
lv = library_versions[diff.version]
try:
lv = library_versions[diff.version]
except KeyError:
# we iterate over all library versions, but for libraries that
# haven't had updates in a release one may not exist for master/develop
return None
lv.insertions = diff.insertions
lv.deletions = diff.deletions
lv.files_changed = diff.files_changed
@@ -509,10 +525,14 @@ class LibraryUpdater:
for item in get_commit_data_for_repo_versions(library.key, min_version):
match item:
case ParsedCommit():
commits_handled += 1
commits.append(handle_commit(item))
commit_item = handle_commit(item)
if commit_item:
commits.append(commit_item)
commits_handled += 1
case VersionDiffStat():
library_version_updates.append(handle_version_diff_stat(item))
lv_update = handle_version_diff_stat(item)
if lv_update:
library_version_updates.append(lv_update)
case _:
assert_never()

View File

@@ -1,46 +1,36 @@
from datetime import date
from django.db import models
from django.db.models import Q, Count
from versions.models import Version
class IssueQuerySet(models.QuerySet):
def closed_during_release(self, version):
def closed_during_release(self, version, prior_version):
"""Get the issues that were closed during a specific version.
Uses the release dates of the version and the prior version and queries for
issues closed in that timeframe.
"""
prior_release = (
Version.objects.minor_versions()
.filter(release_date__lt=version.release_date)
.order_by("-release_date")
.first()
)
if not prior_release:
return self.none()
release_date = version.release_date
if version.name == "master":
release_date = date.today()
return self.filter(
closed__gte=prior_release.release_date, closed__lt=version.release_date
closed__gte=prior_version.release_date, closed__lt=release_date
)
def opened_during_release(self, version):
def opened_during_release(self, version, prior_version):
"""Get the issues that were created during a specific version release.
Uses the release dates of the version and the prior version and queries for
issues created in that timeframe.
"""
prior_release = (
Version.objects.minor_versions()
.filter(release_date__lt=version.release_date)
.order_by("-release_date")
.first()
)
if not prior_release:
return self.none()
release_date = version.release_date
if version.name == "master":
release_date = date.today()
return self.filter(
created__gte=prior_release.release_date, created__lt=version.release_date
created__gte=prior_version.release_date, created__lt=release_date
)
@@ -48,35 +38,32 @@ class IssueManager(models.Manager):
def get_queryset(self):
return IssueQuerySet(self.model, using=self._db)
def closed_during_release(self, version):
return self.get_queryset().closed_during_release(version)
def closed_during_release(self, version, prior_version):
return self.get_queryset().closed_during_release(version, prior_version)
def opened_during_release(self, version):
return self.get_queryset().opened_during_release(version)
def opened_during_release(self, version, prior_version):
return self.get_queryset().opened_during_release(version, prior_version)
def count_opened_closed_during_release(self, version):
def count_opened_closed_during_release(self, version, prior_version):
if version is None:
return self.get_queryset().none()
qs = self.get_queryset()
prior_release = (
Version.objects.minor_versions()
.filter(release_date__lt=version.release_date)
.order_by("-release_date")
.first()
)
if not prior_release:
return qs.none()
release_date = version.release_date
if version.name == "master":
release_date = date.today()
return qs.values("library_id").annotate(
opened=Count(
"id",
filter=Q(
created__gte=prior_release.release_date,
created__lt=version.release_date,
created__gte=prior_version.release_date,
created__lt=release_date,
),
),
closed=Count(
"id",
filter=Q(
closed__gte=prior_release.release_date,
closed__lt=version.release_date,
closed__gte=prior_version.release_date,
closed__lt=release_date,
),
),
)

View File

@@ -1,3 +1,4 @@
from datetime import date
from itertools import pairwise
import djclick as click
import psycopg2
@@ -57,9 +58,10 @@ def create_emaildata(conn: Connection):
versions = Version.objects.minor_versions().order_by("version_array")
columns = ["email", "name", "count"]
versions = list(versions) + [Version.objects.get(name="master")]
for a, b in pairwise(versions):
start = a.release_date
end = b.release_date
end = b.release_date or date.today()
if not (start and end):
raise ValueError("All x.x.0 versions must have a release date.")
with conn.cursor(name=f"emaildata_sync_{b.name}") as cursor:

View File

@@ -2,7 +2,7 @@ import base64
import io
import logging
import random
from datetime import datetime, timedelta
from datetime import datetime, timedelta, date
import psycopg2
from django.conf import settings
@@ -21,7 +21,9 @@ from versions.models import Version
logger = logging.getLogger(__name__)
def generate_wordcloud(version: Version) -> tuple[str | None, list]:
def generate_wordcloud(
version: Version, prior_version: Version
) -> tuple[str | None, list]:
"""Generates a wordcloud png and returns it as a base64 string and word frequencies.
Returns:
@@ -42,7 +44,7 @@ def generate_wordcloud(version: Version) -> tuple[str | None, list]:
font_path=font_full_path,
)
word_frequencies = {}
for content in get_mail_content(version):
for content in get_mail_content(version, prior_version):
for key, val in wc.process_text(content).items():
if len(key) < 2:
continue
@@ -104,13 +106,7 @@ def grey_color_func(*args, **kwargs):
return "hsl(0, 0%%, %d%%)" % random.randint(10, 80)
def get_mail_content(version: Version):
prior_version = (
Version.objects.minor_versions()
.filter(version_array__lt=version.cleaned_version_parts_int)
.order_by("-release_date")
.first()
)
def get_mail_content(version: Version, prior_version: Version):
if not prior_version or not settings.HYPERKITTY_DATABASE_NAME:
return []
conn = psycopg2.connect(settings.HYPERKITTY_DATABASE_URL)
@@ -120,7 +116,10 @@ def get_mail_content(version: Version):
SELECT content FROM hyperkitty_email
WHERE date >= %(start)s AND date < %(end)s;
""",
{"start": prior_version.release_date, "end": version.release_date},
{
"start": prior_version.release_date,
"end": version.release_date or date.today(),
},
)
for [content] in cursor:
yield content

View File

@@ -41,6 +41,7 @@ body {
</style>
{% endblock css %}
{% block content %}
{% now "F j, Y" as today %}
{% with bg_color='' %}
<div>
<div class="pdf-page grid grid-cols-2 gap-x-4 items-center justify-items-center {{ bg_color }}" style="background-image: url('{% static 'img/release_report/bg6.png' %}')">
@@ -51,12 +52,13 @@ body {
class="mt-[3px]"
style="width:3.3rem; margin-right:.5rem;" src="{% static 'img/Boost_Symbol_Transparent.svg' %}"
>
<span class="font-bold">Boost</span>&nbsp;<span class="text-[2.75rem] self-end mb-[2px]">{{ version.display_name }}</span>
<span class="font-bold">Boost</span>&nbsp;<span class="text-[2.75rem] self-end mb-[2px]">{{ report_configuration.display_name }}</span>
</h1>
<div class="flex gap-x-12 link-icons my-4 text-2xl justify-between">
{% include "includes/_social_icon_links.html" %}
</div>
<div><span class="font-bold">{{ commit_count|intcomma }}</span> commit{{ commit_count|pluralize }} up through {{ version.display_name }}</div>
<div class="mb-2" class="font-bold">{% firstof version.release_date today %}</div>
<div><span class="font-bold">{{ commit_count|intcomma }}</span> commit{{ commit_count|pluralize }} up through {{ report_configuration.display_name }}</div>
<div><span class="font-bold">{{ lines_added|intcomma }}</span> line{{ lines_added|pluralize }} added, <span class="font-bold">{{ lines_removed|intcomma }}</span> line{{ lines_removed|pluralize }} removed</div>
<div><span class="font-bold">{{ version_commit_count|intcomma }}</span> new commit{{ version_commit_count|pluralize }} in all library repositories</div>
<div><span class="font-bold">{{ commit_contributors_release_count }}</span> commit contributors, <span class="font-bold">{{ commit_contributors_new_count }}</span> new</div>
@@ -102,10 +104,10 @@ body {
</div>
<div class="flex flex-col h-full justify-between">
{% if version.release_report_cover_image and version.release_report_cover_image.url %}
{% if report_configuration.release_report_cover_image and report_configuration.release_report_cover_image.url %}
<img
class="max-h-[60%]"
src="{{ version.release_report_cover_image.url|strip_query_string }}"
src="{{ report_configuration.release_report_cover_image.url|strip_query_string }}"
alt="release report cover image"
>
{% endif %}
@@ -151,7 +153,7 @@ body {
</div>
</div>
{% if version.sponsor_message %}
{% if report_configuration.sponsor_message %}
<div id="fiscal_committee_page" class="pdf-page !p-8 {{ bg_color }} bg-gray-200 relative">
<h2 class="mt-0">From the Fiscal Sponsorship Committee</h2>
<div class="flex flex-col w-full h-[85%] sponsor-message relative">
@@ -169,7 +171,7 @@ body {
d="M 28.068153,0 H 471.93339 c 17.18711,0 28.0676,10.63409 28.0674,26.12903 l -8e-4,56.64968 c 0,16.63468 -9.6004,26.12907 -28.0674,26.12907 -144.5008,3.63566 -356.11545,0.75984 -445.23187,0 -15.3824,0 -26.70095,-10.79719 -26.70074,-26.12907 l 7.8e-4,-56.64968 c 1.8e-4,-13.27403 12.64918,-26.12903 28.06737,-26.12903 z"
/>
</svg>
<div class="sponsor_message_copy dynamic-text p-[30px] mr-16 absolute z-20 max-h-[340px]">{{ version.sponsor_message|safe }}</div>
<div class="sponsor_message_copy dynamic-text p-[30px] mr-16 absolute z-20 max-h-[340px]">{{ report_configuration.sponsor_message|safe }}</div>
<div class="committee_members flex flex-wrap mt-2 text-sm text-center absolute" style="">
{% for user in committee_members|dictsort:"display_name" %}
<figure class="w-32 m-2">
@@ -247,12 +249,12 @@ body {
{% else %}
no
{% endif %}
mailing list post{{ mailinglist_total|pluralize }} in version&nbsp;{{ version.display_name }} and
mailing list post{{ mailinglist_total|pluralize }} in version&nbsp;{{ report_configuration.display_name }} and
<span class="font-bold">{{ mailinglist_contributor_release_count }}</span>
poster{{ mailinglist_contributor_release_count|pluralize }}
in this version. (<span class="font-bold">{{ mailinglist_contributor_new_count }}</span> New)
</div>
<div class="text-center my-2">Weekly mailing list posts from {{prior_version.release_date}} to {{version.release_date}} on the Boost Developers mailing list.</div>
<div class="text-center my-2">Weekly mailing list posts from {{prior_version.release_date}} to {% firstof version.release_date today %} on the Boost Developers mailing list.</div>
<div id="release_post_stats"></div>
</div>
</div>
@@ -261,7 +263,7 @@ body {
<div class="pdf-page flex {{ bg_color }}" style="background-image: url('{% static 'img/release_report/bg6.png' %}');">
<div class="flex flex-col h-full mx-auto w-full">
<h2 class="mx-auto mb-10">Mailing List New Subscribers</h2>
<div class="text-center my-2">Mailing list new subscribers from from {{prior_version.release_date}} to {{version.release_date}} on the Boost Developers mailing list.</div>
<div class="text-center my-2">Mailing list new subscribers from from {{prior_version.release_date}} to {% firstof version.release_date today %} on the Boost Developers mailing list.</div>
<div id="subscriptions_stats"></div>
</div>
@@ -403,7 +405,7 @@ body {
{{ item.version_count.commit_count|pluralize:"was,were" }}
<span class="font-bold">{{ item.version_count.commit_count }}</span>
commit{{ item.version_count.commit_count|pluralize }}
in release {{ version.display_name }}
in release {{ report_configuration.display_name }}
</div>
{% with insertions=item.library_version.insertions deletions=item.library_version.deletions %}
<div>

View File

@@ -99,3 +99,9 @@ class ReviewResultAdmin(admin.ModelAdmin):
def get_queryset(self, request: HttpRequest) -> QuerySet:
return super().get_queryset(request).select_related("review")
@admin.register(models.ReportConfiguration)
class ReportConfigurationAdmin(admin.ModelAdmin):
list_display = ["version"]
filter_horizontal = ["financial_committee_members"]

View File

@@ -0,0 +1,58 @@
# Generated by Django 4.2.16 on 2025-07-03 21:09
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("versions", "0018_version_financial_committee_members"),
]
operations = [
migrations.CreateModel(
name="ReportConfiguration",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"version",
models.CharField(
help_text="The version name this report configuration is for.",
max_length=256,
),
),
(
"release_report_cover_image",
models.ImageField(
blank=True, null=True, upload_to="release_report_cover/"
),
),
(
"sponsor_message",
models.TextField(
blank=True,
default="",
help_text='Message to show in release reports on the "Fiscal Sponsorship Committee" page.',
),
),
(
"financial_committee_members",
models.ManyToManyField(
blank=True,
help_text="Financial Committee members who are responsible for this release.",
to=settings.AUTH_USER_MODEL,
),
),
],
),
]

View File

@@ -0,0 +1,34 @@
# Generated by Django 4.2.16 on 2025-07-03 21:12
from django.db import migrations
def copy_report_configuration(apps, schema_editor):
ReportConfiguration = apps.get_model("versions", "ReportConfiguration")
Version = apps.get_model("versions", "Version")
version_data = [ {
"name": vd.name,
"release_report_cover_image": vd.release_report_cover_image,
"sponsor_message": vd.sponsor_message,
"financial_committee_members": list(vd.financial_committee_members.values_list("id", flat=True)),
} for vd in Version.objects.all().prefetch_related("financial_committee_members")]
for vd in version_data:
configuration = ReportConfiguration.objects.create(
version=vd["name"],
release_report_cover_image=vd["release_report_cover_image"],
sponsor_message=vd["sponsor_message"],
)
configuration.financial_committee_members.set(vd["financial_committee_members"])
def drop_report_configuration(apps, schema_editor):
ReportConfiguration = apps.get_model("versions", "ReportConfiguration")
output = ReportConfiguration.objects.all().delete()
print(f"\nDeleted {output}...")
class Migration(migrations.Migration):
dependencies = [
("versions", "0019_reportconfiguration"),
]
operations = [migrations.RunPython(copy_report_configuration, drop_report_configuration)]

View File

@@ -282,3 +282,40 @@ class ReviewResult(models.Model):
)
sibling_results.update(is_most_recent=False)
super().save(*args, **kwargs)
class ReportConfiguration(models.Model):
"""
Configuration for release reports. Used so these can be set in advance of a version
being generated and then used by that version's release report.
"""
version = models.CharField(
max_length=256,
null=False,
blank=False,
help_text="The version name for this report configuration. e.g. boost-1.75.0",
unique=True,
)
release_report_cover_image = models.ImageField(
null=True,
blank=True,
upload_to="release_report_cover/",
)
sponsor_message = models.TextField(
default="",
blank=True,
help_text='Message to show in release reports on the "Fiscal Sponsorship Committee" page.', # noqa: E501
)
financial_committee_members = models.ManyToManyField(
User,
blank=True,
help_text="Financial Committee members who are responsible for this release.",
)
@cached_property
def display_name(self):
return self.version.replace("boost-", "")
def __str__(self):
return self.version