mirror of
https://github.com/boostorg/website-v2.git
synced 2026-01-19 04:42:17 +00:00
Decouple release reports from releases (#1737)
This commit is contained in:
@@ -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`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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> <span class="text-[2.75rem] self-end mb-[2px]">{{ version.display_name }}</span>
|
||||
<span class="font-bold">Boost</span> <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 {{ version.display_name }} and
|
||||
mailing list post{{ mailinglist_total|pluralize }} in version {{ 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>
|
||||
|
||||
@@ -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"]
|
||||
|
||||
58
versions/migrations/0019_reportconfiguration.py
Normal file
58
versions/migrations/0019_reportconfiguration.py
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -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)]
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user