Skip to content

Commit d280ca3

Browse files
authored
Improve logging (#259)
* Improve logging * Fix tests * Fix test coverage * misc
1 parent 5f4e21b commit d280ca3

7 files changed

Lines changed: 93 additions & 100 deletions

File tree

config/settings/local.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@
7676
import socket
7777

7878
hostname, _, ips = socket.gethostbyname_ex(socket.gethostname())
79-
INTERNAL_IPS += [".".join(ip.split(".")[:-1] + ["1"]) for ip in ips]
79+
INTERNAL_IPS += [".".join([*ip.split(".")[:-1], "1"]) for ip in ips]
8080
with contextlib.suppress(socket.gaierror):
8181
_, _, ips = socket.gethostbyname_ex("node") # type: ignore[assignment]
8282
INTERNAL_IPS.extend(ips)

democrasite/webiscite/constitution.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
"""This module handles the logic behind the constitution.
22
3-
This contains methods to read the constitution as a dict, check which files in
4-
a diff contain constitutional amendments, and automatically update the
5-
constitution for changes which edit files included in the constitution without
6-
resulting in constitutional amendments
3+
This contains functions to read the constitution as json, check which files in a diff
4+
contain constitutional amendments, and automatically update the constitution for changes
5+
that moved constitutionally protected sections without editing them
76
"""
87

98
import json

democrasite/webiscite/models.py

Lines changed: 28 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
"""Models for the webiscite app"""
22

33
import json
4+
import logging
45
from collections.abc import Iterator
56
from contextlib import contextmanager
6-
from logging import getLogger
77
from typing import Any
88

99
from django.conf import settings
@@ -20,7 +20,7 @@
2020

2121
from .constitution import is_constitutional
2222

23-
logger = getLogger(__name__)
23+
logger = logging.getLogger(__name__)
2424

2525

2626
class ClosedBillVoteError(Exception):
@@ -53,8 +53,7 @@ def create_from_github(self, pr: dict[str, Any]) -> T:
5353
},
5454
)
5555

56-
action = "created" if created else "updated"
57-
logger.info("PR %s: Pull request %s", pr["number"], action)
56+
pull_request.log("%s", "Created" if created else "Updated")
5857
return pull_request
5958

6059

@@ -86,6 +85,10 @@ class PullRequest(TimeStampedModel):
8685
def __str__(self) -> str:
8786
return f"PR #{self.number}"
8887

88+
def log(self, msg, *args, level=logging.INFO):
89+
logger.log(level, f"PR #%s: {msg}", self.number, *args) # noqa: G004
90+
# f-string necessary to let string interpolation work in msg
91+
8992
@property
9093
def diff_link(self) -> str:
9194
"""Return base url of PR by removing ".diff" extension"""
@@ -106,7 +109,7 @@ def close(self) -> "Bill | None":
106109
try:
107110
bill: Bill = self.bill_set.get(status=Bill.Status.OPEN)
108111
except Bill.DoesNotExist:
109-
logger.info("PR %s: No open bill found", self.number)
112+
self.log("No open bill found")
110113
return None
111114
else:
112115
bill.close()
@@ -141,6 +144,7 @@ def get_queryset(self):
141144
return (
142145
super()
143146
.get_queryset()
147+
.select_related("pull_request")
144148
.annotate(
145149
total_votes=models.Count("vote"),
146150
yes_percent=models.Case(
@@ -203,7 +207,7 @@ def create_from_github(
203207
applicable
204208
"""
205209
with self._create_submit_task() as submit_task:
206-
self._bill = self.model(
210+
self._bill: Bill = self.model(
207211
name=title,
208212
description=body,
209213
author=author,
@@ -214,10 +218,10 @@ def create_from_github(
214218
)
215219
self._bill.full_clean()
216220
self._bill.save()
217-
logger.info("PR %s: Bill %s created", pull_request.number, self._bill.id)
218221
bill = self._bill
222+
bill.log("Created")
219223

220-
return bill # noqa: RET504
224+
return bill
221225

222226
@contextmanager
223227
def _create_submit_task(self) -> Iterator[PeriodicTask]:
@@ -255,12 +259,9 @@ def _create_submit_task(self) -> Iterator[PeriodicTask]:
255259
submit_task.name = f"bill_submit:{self._bill.id}"
256260
submit_task.args = json.dumps([self._bill.id])
257261
submit_task.save()
258-
logger.info(
259-
"PR %s: Scheduled %s",
260-
self._bill.pull_request.number,
261-
submit_task.name,
262-
)
262+
self._bill.log("Scheduled %s", submit_task.name)
263263

264+
# Attribute could be shared between model instances
264265
del self._bill
265266

266267

@@ -322,6 +323,10 @@ class Meta:
322323
def __str__(self) -> str:
323324
return f"Bill {self.id}: {self.name} ({self.pull_request})"
324325

326+
def log(self, msg, *args):
327+
logger.info(f"Bill %s (#%s): {msg}", self.id, self.pull_request.number, *args) # noqa: G004
328+
# f-string necessary to let string interpolation work in msg
329+
325330
def get_absolute_url(self) -> str:
326331
"""Returns URL to view this Bill instance"""
327332
return reverse("webiscite:bill-detail", kwargs={"pk": self.id})
@@ -359,39 +364,21 @@ def vote(self, user: User, *, support: bool) -> None:
359364
if self.status != self.Status.OPEN:
360365
raise ClosedBillVoteError("Bill is not open for voting")
361366

367+
supports = "yes" if support else "no"
362368
try:
363369
vote: Vote = self.vote_set.get(user=user)
364370
if vote.support == support:
365371
vote.delete()
366-
logger.info(
367-
'PR %s: User %s retracted their vote "%s" on bill %s',
368-
self.pull_request.number,
369-
user.username,
370-
"yes" if support else "no",
371-
self.id,
372-
)
372+
self.log("%s retracted their %s vote", user.username, supports)
373373

374374
else:
375375
vote.support = support
376376
vote.save(update_fields=["support", "when"]) # Ensure "when" is updated
377-
logger.info(
378-
"PR %s: User %s changed their vote on bill %s from %s to %s",
379-
self.pull_request.number,
380-
user.username,
381-
self.id,
382-
not support,
383-
support,
384-
)
377+
self.log("%s changed their vote to %s", user.username, supports)
385378

386379
except Vote.DoesNotExist:
387380
self.votes.add(user, through_defaults={"support": support})
388-
logger.info(
389-
"PR %s: User %s voted %s on bill %s",
390-
self.pull_request.number,
391-
user.username,
392-
"yes" if support else "no",
393-
self.id,
394-
)
381+
self.log("%s voted %s", user.username, supports)
395382

396383
def user_supports(self, user: User) -> bool | None:
397384
"""
@@ -415,21 +402,17 @@ def close(self) -> None:
415402
"""Close the bill and disable its submit task"""
416403
self.status = self.Status.CLOSED
417404
self.save()
418-
logger.info("Bill %s set to closed", self.id)
405+
self.log("Closed")
419406

420407
self._submit_task.enabled = False
421408
self._submit_task.save()
422-
logger.info("Submit task for bill %s disabled", self.id)
409+
self.log("Submit task disabled")
423410

424411
def submit(self) -> None:
425412
"""Check if the bill has enough votes to pass and update the status"""
426413
# Bill was closed before voting period ended
427414
if self.status != Bill.Status.OPEN:
428-
logger.info(
429-
"PR %s: bill %s was not open when submitted",
430-
self.pull_request.number,
431-
self.id,
432-
)
415+
self.log("Bill was not open when submitted")
433416
return
434417

435418
self.status = self._check_approval()
@@ -438,11 +421,7 @@ def submit(self) -> None:
438421
def _check_approval(self) -> "Bill.Status":
439422
total_votes = self.votes.count()
440423
if total_votes < settings.WEBISCITE_MINIMUM_QUORUM:
441-
logger.info(
442-
"PR %s: bill %s rejected due to insufficient votes",
443-
self.pull_request.number,
444-
self.id,
445-
)
424+
self.log("Rejected due to insufficient votes")
446425
return self.Status.FAILED
447426

448427
approval = self.yes_votes.count() / total_votes
@@ -452,18 +431,8 @@ def _check_approval(self) -> "Bill.Status":
452431
approved = approval > settings.WEBISCITE_NORMAL_MAJORITY
453432

454433
if not approved:
455-
logger.info(
456-
"PR %s: bill %s rejected with %s%% approval",
457-
self.pull_request.number,
458-
self.id,
459-
approval * 100,
460-
)
434+
self.log("Rejected with %s%% approval", approval * 100)
461435
return self.Status.REJECTED
462436

463-
logger.info(
464-
"PR %s: bill %s approved with %s%% approval",
465-
self.pull_request.number,
466-
self.id,
467-
approval,
468-
)
437+
self.log("Approved with %s%% approval", approval * 100)
469438
return self.Status.APPROVED

democrasite/webiscite/tasks.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
from . import constitution
1717
from .models import Bill
1818

19-
# TODO (#58): Improve logging
2019
logger = get_task_logger(__name__)
2120

2221

@@ -36,19 +35,20 @@ def submit_bill(bill_id: int) -> None:
3635
bill = Bill.objects.get(pk=bill_id)
3736
bill.submit()
3837

39-
repo = Github(auth=Auth.Token(settings.WEBISCITE_GITHUB_TOKEN)).get_repo(
40-
settings.WEBISCITE_REPO
41-
)
38+
github_auth = Auth.Token(settings.WEBISCITE_GITHUB_TOKEN)
39+
repo = Github(auth=github_auth).get_repo(settings.WEBISCITE_REPO)
4240
pull = repo.get_pull(bill.pull_request.number)
4341

4442
if bill.status != Bill.Status.APPROVED:
4543
pull.edit(state="closed") # Close failed pull request
4644
return
4745

48-
merge = pull.merge(merge_method="squash", sha=bill.pull_request.sha)
49-
logger.info(
50-
"PR %s: bill %s merged (%s)", bill.pull_request.number, bill.id, merge.merged
51-
)
46+
merged = pull.merge(merge_method="squash", sha=bill.pull_request.sha)
47+
if merged:
48+
bill.log("Merged")
49+
else:
50+
logger.warning("Bill %s failed to merge", bill.id)
51+
return
5252

5353
# Automatically update constitution line numbers if necessary
5454
_update_constitution(bill, repo)
@@ -75,4 +75,4 @@ def _update_constitution(bill: Bill, repo: Repository) -> None:
7575
content=con_update,
7676
sha=con_sha,
7777
)
78-
logger.info("PR %s: constitution updated", bill.pull_request.number)
78+
bill.log("Constitution updated")

democrasite/webiscite/tests/test_models.py

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,22 @@
2121
class TestPullRequestManager:
2222
def test_create_from_github_create(self, caplog):
2323
pr = GithubPullRequestFactory.create(number=FAKE.random_int())
24-
pull_request = PullRequest.objects.create_from_github(pr)
25-
assert any(
26-
record.message == f"PR {pull_request.number}: Pull request created"
27-
for record in caplog.records
28-
)
24+
assert not PullRequest.objects.filter(title=pr["title"]).exists()
25+
26+
pull_request: PullRequest = PullRequest.objects.create_from_github(pr)
27+
28+
assert hasattr(pull_request, "created")
29+
assert PullRequest.objects.filter(title=pr["title"]).exists()
2930

3031
def test_create_from_github_update(self, bill: Bill, caplog):
31-
# PullRequest is created in the BillFactory
32+
# PullRequest is created during creation of bill fixture
3233
pr = GithubPullRequestFactory.create(bill=bill)
33-
pull_request = PullRequest.objects.create_from_github(pr)
34-
assert any(
35-
record.message == f"PR {pull_request.number}: Pull request updated"
36-
for record in caplog.records
37-
)
34+
assert PullRequest.objects.filter(title=bill.pull_request.title).exists()
35+
pr["title"] = "New Title"
36+
37+
PullRequest.objects.create_from_github(pr)
38+
39+
assert PullRequest.objects.filter(title="New Title").exists()
3840

3941

4042
class TestPullRequest:
@@ -51,14 +53,11 @@ def test_close_no_bill(self, caplog):
5153
pull_request = PullRequestFactory.create()
5254
assert pull_request.status == "open"
5355

54-
pull_request.close()
56+
bill = pull_request.close()
5557

5658
pull_request.refresh_from_db()
5759
assert pull_request.status == "closed"
58-
assert any(
59-
record.message == f"PR {pull_request.number}: No open bill found"
60-
for record in caplog.records
61-
)
60+
assert bill is None
6261

6362

6463
class TestVote:
@@ -133,7 +132,7 @@ def test__create_submit_task(self):
133132
with (
134133
pytest.raises(
135134
AttributeError,
136-
match="self._bill was not saved in the submit task context",
135+
match=r"self._bill was not saved in the submit task context",
137136
),
138137
Bill.objects._create_submit_task(), # noqa: SLF001
139138
):

democrasite/webiscite/tests/test_tasks.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,23 @@ def test_bill_passed(self, mock_token, mock_repo):
4040
merge_method="squash", sha=bill.pull_request.sha
4141
)
4242

43+
@patch("github.Github.get_repo")
44+
@patch("github.Auth.Token", spec=True)
45+
def test_bill_merge_failed(self, mock_token, mock_repo):
46+
bill = BillFactory.create(constitutional=True)
47+
# constitutional so _update_constitution isn't called
48+
voters = UserFactory.create_batch(settings.WEBISCITE_MINIMUM_QUORUM)
49+
Vote.objects.bulk_create(
50+
[Vote(bill=bill, user=voter, support=True) for voter in voters]
51+
)
52+
mock_repo().get_pull().merge.return_value = None
53+
54+
submit_bill(bill.id)
55+
56+
mock_repo().get_pull().merge.assert_called_once_with(
57+
merge_method="squash", sha=bill.pull_request.sha
58+
)
59+
4360
@patch.object(constitution, "update_constitution")
4461
@patch("requests.get")
4562
@patch("github.Repository.Repository")

0 commit comments

Comments
 (0)