Skip to content

Commit da39d8e

Browse files
authored
Add "Import new releases" emails (boostorg#1872) (boostorg#1945)
1 parent 26c4da0 commit da39d8e

File tree

6 files changed

+239
-112
lines changed

6 files changed

+239
-112
lines changed

core/management/actions.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
from abc import ABCMeta, abstractmethod
2+
from dataclasses import dataclass
3+
from typing import Callable
4+
5+
import djclick as click
6+
from django.core.mail import send_mail
7+
from django.core.management import call_command
8+
from django.utils import timezone
9+
10+
from config import settings
11+
12+
13+
def progress_message(message: str):
14+
click.secho(message, fg="green")
15+
return f"{timezone.now()}: {message}"
16+
17+
18+
@dataclass
19+
class Action:
20+
"""
21+
A distinct task to be completed.
22+
23+
Action can be a callable or a list of string arguments to pass to `call_command`
24+
"""
25+
26+
description: str
27+
handler: Callable | list[str]
28+
29+
@property
30+
def handler_name(self) -> str:
31+
if isinstance(self.handler, Callable):
32+
return f"function: {self.handler.__name__}"
33+
return f"command: {self.handler[0]}"
34+
35+
def run(self):
36+
if isinstance(self.handler, Callable):
37+
self.handler()
38+
else:
39+
call_command(*self.handler)
40+
41+
42+
class ActionsManager(metaclass=ABCMeta):
43+
progress_messages: list[str] = []
44+
45+
def __init__(self):
46+
self.tasks: list[Action] = []
47+
self.set_tasks()
48+
self.validate_tasks()
49+
50+
@abstractmethod
51+
def set_tasks(self):
52+
"""
53+
Set self.tasks to a list of Action instances.
54+
self.tasks = [Action(...), Action(...)]
55+
"""
56+
raise NotImplementedError
57+
58+
def validate_tasks(self):
59+
if not self.tasks:
60+
raise ValueError("No tasks defined. You must set some with set_tasks()")
61+
if not all(isinstance(task, Action) for task in self.tasks):
62+
raise TypeError("All tasks must be instances of Action")
63+
64+
def add_progress_message(self, message: str):
65+
message = progress_message(message)
66+
self.progress_messages.append(message)
67+
68+
def run_tasks(self) -> dict[str:int]:
69+
for task in self.tasks:
70+
# "Task: " prefix for easy log parsing
71+
self.add_progress_message(
72+
f"Task start - {task.handler_name}, desc: {task.description.lower()}..."
73+
)
74+
task.run()
75+
self.add_progress_message(
76+
f"Task done - {task.handler_name}, desc: {task.description.lower()}"
77+
)
78+
79+
80+
def send_notification(user, message, subject):
81+
if user and user.email:
82+
send_mail(
83+
subject,
84+
message,
85+
settings.DEFAULT_FROM_EMAIL,
86+
[user.email],
87+
)
Lines changed: 48 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
11
import traceback
22
from contextlib import suppress
3-
from dataclasses import dataclass
43
from datetime import timedelta
5-
from typing import Callable
64

75
import djclick as click
86

9-
from django.core.mail import send_mail
107
from django.utils import timezone
118
from django.core.management import call_command
129
from django.contrib.auth import get_user_model
1310
from django.conf import settings
1411
from slack_sdk.errors import SlackApiError
1512

1613
from core.githubhelper import GithubAPIClient
14+
from core.management.actions import (
15+
progress_message,
16+
Action,
17+
ActionsManager,
18+
send_notification,
19+
)
1720
from libraries.forms import CreateReportForm
1821
from libraries.tasks import update_commits
1922
from reports.models import WebsiteStatReport
@@ -23,82 +26,39 @@
2326
User = get_user_model()
2427

2528

26-
def send_notification(user, message, subject="Task Started: release_tasks"):
27-
if user.email:
28-
send_mail(
29-
subject,
30-
message,
31-
settings.DEFAULT_FROM_EMAIL,
32-
[user.email],
33-
)
34-
35-
36-
def progress_message(message: str):
37-
click.secho(message, fg="green")
38-
return f"{timezone.now()}: {message}"
39-
40-
41-
@dataclass
42-
class ReleaseTask:
43-
"""
44-
A distinct task to be completed.
45-
46-
Action can be a callable or a list of string arguments to pass to `call_command`
47-
"""
48-
49-
description: str
50-
action: Callable | list[str]
51-
52-
def run(self):
53-
if isinstance(self.action, Callable):
54-
self.action()
55-
else:
56-
call_command(*self.action)
57-
58-
59-
class ReleaseTasksManager:
29+
class ReleaseTasksManager(ActionsManager):
6030
latest_version: Version | None = None
61-
progress_messages: list[str] = []
6231
handled_commits: dict[str, int] = {}
6332

6433
def __init__(self, should_generate_report: bool = False):
6534
self.should_generate_report = should_generate_report
35+
super().__init__()
36+
37+
def set_tasks(self):
6638
self.tasks = [
67-
ReleaseTask("Importing versions", self.import_versions),
68-
ReleaseTask(
39+
Action("Importing versions", self.import_versions),
40+
Action(
6941
"Importing most recent beta version",
7042
["import_beta_release", "--delete-versions"],
7143
),
72-
ReleaseTask("Importing libraries", ["update_libraries"]),
73-
ReleaseTask(
44+
Action("Importing libraries", ["update_libraries"]),
45+
Action(
7446
"Saving library-version relationships", self.import_library_versions
7547
),
76-
ReleaseTask("Adding library maintainers", ["update_maintainers"]),
77-
ReleaseTask("Adding library authors", ["update_authors"]),
78-
ReleaseTask(
48+
Action("Adding library maintainers", ["update_maintainers"]),
49+
Action("Adding library authors", ["update_authors"]),
50+
Action(
7951
"Adding library version authors", ["update_library_version_authors"]
8052
),
81-
ReleaseTask("Importing git commits", self.handle_commits),
82-
ReleaseTask("Syncing mailinglist statistics", ["sync_mailinglist_stats"]),
83-
ReleaseTask("Updating github issues", ["update_issues"]),
84-
ReleaseTask("Updating slack activity buckets", ["fetch_slack_activity"]),
85-
ReleaseTask("Updating website statistics", self.update_website_statistics),
86-
ReleaseTask("Importing mailing list counts", self.import_ml_counts),
87-
ReleaseTask("Generating report", self.generate_report),
53+
Action("Importing git commits", self.import_commits),
54+
Action("Syncing mailinglist statistics", ["sync_mailinglist_stats"]),
55+
Action("Updating github issues", ["update_issues"]),
56+
Action("Updating slack activity buckets", ["fetch_slack_activity"]),
57+
Action("Updating website statistics", self.update_website_statistics),
58+
Action("Importing mailing list counts", self.import_ml_counts),
59+
Action("Generating report", self.generate_report),
8860
]
8961

90-
def update_release_data(self) -> dict[str:int]:
91-
for task in self.tasks:
92-
# "Release Task: " prefix for easy log parsing
93-
self.progress_messages.append(
94-
progress_message(f"Release Task: {task.description}...")
95-
)
96-
task.run()
97-
self.progress_messages.append(
98-
progress_message(f"Release Task: Finished {task.description.lower()}")
99-
)
100-
return self.handled_commits
101-
10262
def import_versions(self):
10363
call_command("import_versions")
10464
self.latest_version = Version.objects.most_recent()
@@ -107,7 +67,7 @@ def import_library_versions(self):
10767
latest_version_number = self.latest_version.name.lstrip("boost-")
10868
call_command("import_library_versions", min_release=latest_version_number)
10969

110-
def handle_commits(self):
70+
def import_commits(self):
11171
self.handled_commits = update_commits(min_version=self.latest_version.name)
11272

11373
def update_website_statistics(self):
@@ -125,9 +85,7 @@ def import_ml_counts(self):
12585

12686
def generate_report(self):
12787
if not self.should_generate_report:
128-
self.progress_messages.append(
129-
progress_message("Skipped - report generation not requested")
130-
)
88+
self.add_progress_message("Skipped - report generation not requested")
13189
return
13290
form = CreateReportForm({"version": self.latest_version.id})
13391
form.cache_html()
@@ -136,11 +94,9 @@ def generate_report(self):
13694
@locked(1138692)
13795
def run_commands(progress: list[str], generate_report: bool = False):
13896
manager = ReleaseTasksManager(should_generate_report=generate_report)
139-
handled_commits = manager.update_release_data()
140-
97+
manager.run_tasks()
14198
progress.extend(manager.progress_messages)
142-
143-
return handled_commits
99+
return manager.handled_commits
144100

145101

146102
def bad_credentials() -> list[str]:
@@ -185,40 +141,38 @@ def command(user_id=None, generate_report=False):
185141
"""A long running chain of tasks to import and update library data."""
186142
start = timezone.now()
187143

188-
user = None
189-
if user_id:
190-
user = User.objects.filter(id=user_id).first()
144+
user = User.objects.filter(id=user_id).first() if user_id else None
191145

192146
progress = ["___Progress Messages___"]
193147
if missing_creds := bad_credentials():
194148
progress.append(
195149
progress_message(f"Missing credentials {', '.join(missing_creds)}")
196150
)
197-
if user:
198-
send_notification(
199-
user,
200-
message="Your task `release_tasks` failed.",
201-
subject="Task Failed: release_tasks",
202-
)
151+
send_notification(
152+
user,
153+
message="Your task `release_tasks` failed.",
154+
subject="Task Failed: release_tasks",
155+
)
203156
return
204-
if user:
205-
send_notification(user, f"Your task `release_tasks` was started at: {start}")
157+
158+
send_notification(
159+
user,
160+
f"Your task `release_tasks` was started at: {start}",
161+
subject="Task Started: release_tasks",
162+
)
206163

207164
try:
208165
handled_commits = run_commands(progress, generate_report)
209166
end = timezone.now()
210-
progress.append(progress_message(f"All done! Completed in {end - start}"))
211167
except Exception:
212168
error = traceback.format_exc()
213169
message = [
214170
f"ERROR: There was an error while running release_tasks.\n\n{error}",
215171
"\n".join(progress),
216172
]
217-
if user:
218-
send_notification(
219-
user,
220-
"\n\n".join(message),
221-
)
173+
send_notification(
174+
user, "\n\n".join(message), subject="Task Failed: release_tasks"
175+
)
222176
raise
223177

224178
zero_commit_libraries = [
@@ -236,9 +190,8 @@ def command(user_id=None, generate_report=False):
236190
for lib, _ in zero_commit_libraries:
237191
zero_commit_message.append(lib)
238192
message.append("\n".join(zero_commit_message))
239-
if user:
240-
send_notification(
241-
user,
242-
"\n\n".join(message),
243-
subject="Task Complete: release_tasks",
244-
)
193+
send_notification(
194+
user,
195+
"\n\n".join(message),
196+
subject="Task Complete: release_tasks",
197+
)

libraries/tasks.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,19 @@ def release_tasks(user_id=None, generate_report=False):
267267
call_command(*command)
268268

269269

270+
@app.task
271+
def import_new_versions_tasks(user_id=None):
272+
"""Call the import_new_versions management command.
273+
274+
If a user_id is given, that user will receive an email at the beginning
275+
and at the end of the task.
276+
"""
277+
command = ["import_new_versions"]
278+
if user_id:
279+
command.extend(["--user_id", user_id])
280+
call_command(*command)
281+
282+
270283
@app.task
271284
def synchronize_commit_author_user_data():
272285
logger.info("Starting synchronize_commit_author_user_data")

versions/admin.py

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,9 @@
44
from django.http import HttpRequest, HttpResponseRedirect
55
from django.urls import path
66

7-
from libraries.tasks import release_tasks
7+
from libraries.tasks import release_tasks, import_new_versions_tasks
88

99
from . import models
10-
from .tasks import (
11-
import_versions,
12-
import_most_recent_beta_release,
13-
import_development_versions,
14-
)
1510

1611

1712
class VersionFileInline(admin.StackedInline):
@@ -57,17 +52,9 @@ def release_tasks(self, request):
5752
return HttpResponseRedirect("../")
5853

5954
def import_new_releases(self, request):
60-
import_versions.delay(new_versions_only=True)
61-
import_most_recent_beta_release.delay(delete_old=True)
62-
# Import the master and develop branches
63-
import_development_versions.delay()
64-
self.message_user(
65-
request,
66-
"""
67-
New releases are being imported. If you don't see any new releases,
68-
please refresh this page or check the logs.
69-
""",
70-
)
55+
import_new_versions_tasks.delay(user_id=request.user.id)
56+
msg = "New releases are being imported. You will receive an email when the task finishes." # noqa: E501
57+
self.message_user(request, msg)
7158
return HttpResponseRedirect("../")
7259

7360

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import djclick as click
2+
3+
from versions.tasks import import_development_versions
4+
5+
6+
@click.command()
7+
def command():
8+
"""
9+
Import development versions from GitHub.
10+
11+
This command will import the master and develop branches as versions
12+
from GitHub based on the BOOST_BRANCHES setting.
13+
"""
14+
click.secho("Importing development versions...", fg="green")
15+
import_development_versions()
16+
click.secho("Finished importing development versions.", fg="green")

0 commit comments

Comments
 (0)