Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions democrasite/templates/webiscite/bill_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ <h2>{{ bill }}</h2>
{% if bill.status != bill.Status.OPEN %}
<p class="{% if bill.status == bill.Status.APPROVED %}
text-success
{% elif bill.status == bill.Status.DRAFT %}
text-secondary
{% else %}
text-danger
{% endif %}
Expand Down
2 changes: 2 additions & 0 deletions democrasite/templates/webiscite/bill_list.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ <h5 class="card-title">{{ bill }}</h5>
{% if bill.status != bill.Status.OPEN %}
<p class="{% if bill.status == bill.Status.APPROVED %}
text-success
{% elif bill.status == bill.Status.DRAFT %}
text-secondary
{% else %}
text-danger
{% endif %}
Expand Down
91 changes: 91 additions & 0 deletions democrasite/webiscite/fixtures/democrasite.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"created": "2024-02-25T17:26:54.006Z",
"modified": "2024-02-25T17:26:54.012Z",
"status": "open",
"draft": false,

"title": "The Initial Act",
"additions": 0,
Expand All @@ -59,6 +60,7 @@
"created": "2024-02-25T17:26:54.006Z",
"modified": "2024-02-25T17:26:54.012Z",
"status": "open",
"draft": false,

"title": "The Test Act",
"additions": 1000,
Expand All @@ -75,6 +77,7 @@
"created": "2024-02-25T17:26:54.006Z",
"modified": "2024-02-25T17:26:54.012Z",
"status": "open",
"draft": false,

"title": "The Finished Act",
"additions": 69,
Expand All @@ -97,6 +100,7 @@
"diff_url": "https://github.com/mfosterw/cookiestocracy/pull/64/files",
"author_name": "matthew",
"status": "open",
"draft": false,
"sha": "123",
"history_date": "2025-09-07T22:52:36.583Z",
"history_change_reason": "",
Expand All @@ -117,6 +121,7 @@
"diff_url": "https://github.com/mfosterw/cookiestocracy/pull/64/files",
"author_name": "octocat",
"status": "open",
"draft": false,
"sha": "1234",
"history_date": "2025-09-07T22:52:36.583Z",
"history_change_reason": "",
Expand All @@ -137,6 +142,7 @@
"diff_url": "https://github.com/mfosterw/cookiestocracy/pull/64/files",
"author_name": "dependabot",
"status": "open",
"draft": false,
"sha": "12345",
"history_date": "2025-09-07T22:52:36.583Z",
"history_change_reason": "",
Expand Down Expand Up @@ -430,5 +436,90 @@
"enabled": false,
"date_changed": "2024-02-20T12:00:22.856Z"
}
},
{
"model": "webiscite.pullrequest",
"pk": -4,
"fields": {
"created": "2026-02-16T12:00:00.000Z",
"modified": "2026-02-16T12:00:00.000Z",
"status": "open",
"draft": true,

"title": "The Draft Act",
"additions": 42,
"deletions": 7,
"diff_url": "https://github.com/mfosterw/cookiestocracy/pull/64.diff",
"author_name": "matthew",
"sha": "abc123"
}
},
{
"model": "webiscite.historicalpullrequest",
"pk": 4,
"fields": {
"created": "2026-02-16T12:00:00.000Z",
"modified": "2026-02-16T12:00:00.000Z",
"number": -4,
"title": "The Draft Act",
"additions": 42,
"deletions": 7,
"diff_url": "https://github.com/mfosterw/cookiestocracy/pull/64.diff",
"author_name": "matthew",
"status": "open",
"draft": true,
"sha": "abc123",
"history_date": "2026-02-16T12:00:00.000Z",
"history_change_reason": "",
"history_type": "+",
"history_user": null
}
},
{
"model": "webiscite.bill",
"pk": 7,
"fields": {
"created": "2026-02-16T12:00:00.000Z",
"modified": "2026-02-16T12:00:00.000Z",
"name": "The Draft Act",
"description": "This bill is a draft. It was created from a draft pull request and cannot be voted on until the PR is marked ready for review.",
"author": 1,
"pull_request": -4,
"status": "draft",
"constitutional": false,
"_submit_task": 7
}
},
{
"model": "webiscite.historicalbill",
"pk": 7,
"fields": {
"id": 7,
"created": "2026-02-16T12:00:00.000Z",
"modified": "2026-02-16T12:00:00.000Z",
"name": "The Draft Act",
"description": "This bill is a draft. It was created from a draft pull request and cannot be voted on until the PR is marked ready for review.",
"status": "draft",
"constitutional": false,
"author": 1,
"pull_request": -4,
"_submit_task": 7,
"history_date": "2026-02-16T12:00:00.000Z",
"history_change_reason": "",
"history_type": "+",
"history_user": null
}
},
{
"model": "django_celery_beat.periodictask",
"pk": 7,
"fields": {
"name": "bill_submit:7",
"task": "democrasite.webiscite.tasks.submit_bill",
"interval": 1,
"args": "[7]",
"enabled": false,
"date_changed": "2026-02-16T12:00:00.000Z"
}
}
]
20 changes: 14 additions & 6 deletions democrasite/webiscite/managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ def create_from_github(self, pr: dict[str, Any]) -> T:
"author_name": pr["user"]["login"],
"status": pr["state"],
"sha": pr["head"]["sha"],
"draft": pr.get("draft", False),
},
)

Expand Down Expand Up @@ -111,21 +112,23 @@ def create_from_github(
GitHub pull request

Args:
pr: The pull request data from the GitHub API
title: The title of the pull request
body: The body of the pull request
author: The user who created the pull request
diff_text: The text of the diff of the pull request
pull_request: The pull request instance to associate with the bill

Returns:
A tuple containing the new or update pull request and new bill instance, if
applicable
The new bill instance
"""
with self._create_submit_task() as submit_task:
draft = pull_request.draft
with self._create_submit_task(enabled=not draft) as submit_task:
self._bill: Bill = self.model(
name=title,
description=body,
author=author,
pull_request=pull_request,
status=self.model.Status.OPEN,
status=self.model.Status.DRAFT if draft else self.model.Status.OPEN,
constitutional=bool(is_constitutional(diff_text)),
_submit_task=submit_task,
)
Expand All @@ -137,9 +140,13 @@ def create_from_github(
return bill

@contextmanager
def _create_submit_task(self) -> Iterator[PeriodicTask]:
def _create_submit_task(self, *, enabled: bool = True) -> Iterator[PeriodicTask]:
"""Schedule a task to submit this bill for voting

Args:
enabled: Whether the task should be enabled immediately. Set to False
for draft bills whose voting period hasn't started yet.

Returns:
The task that was scheduled
"""
Expand All @@ -153,6 +160,7 @@ def _create_submit_task(self) -> Iterator[PeriodicTask]:
name="bill_submit:temp",
task="democrasite.webiscite.tasks.submit_bill",
one_off=True,
enabled=enabled,
# If last_run_at is not set, the task will run relative to when the
# scheduler started, not when it was created
last_run_at=timezone.now(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Generated by Django 5.2.11 on 2026-02-16 23:17

from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('django_celery_beat', '0019_alter_periodictasks_options'),
('webiscite', '0005_alter_bill_name_alter_historicalbill_name_and_more'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.RemoveConstraint(
model_name='bill',
name='unique_open_pull_request',
),
migrations.AddField(
model_name='historicalpullrequest',
name='draft',
field=models.BooleanField(default=False, help_text='Whether the pull request is a draft on GitHub'),
),
migrations.AddField(
model_name='pullrequest',
name='draft',
field=models.BooleanField(default=False, help_text='Whether the pull request is a draft on GitHub'),
),
migrations.AlterField(
model_name='bill',
name='status',
field=models.CharField(choices=[('draft', 'Draft'), ('open', 'Open'), ('approved', 'Approved'), ('rejected', 'Rejected'), ('failed', 'Not Enough Votes'), ('closed', 'PR Closed')], default='open', help_text='The current status of the bill', max_length=10),
),
migrations.AlterField(
model_name='historicalbill',
name='status',
field=models.CharField(choices=[('draft', 'Draft'), ('open', 'Open'), ('approved', 'Approved'), ('rejected', 'Rejected'), ('failed', 'Not Enough Votes'), ('closed', 'PR Closed')], default='open', help_text='The current status of the bill', max_length=10),
),
migrations.AddConstraint(
model_name='bill',
constraint=models.UniqueConstraint(condition=models.Q(('status__in', ['open', 'draft'])), fields=('pull_request',), name='unique_active_pull_request', violation_error_message='A Bill for this pull request is already active'),
),
]
28 changes: 24 additions & 4 deletions democrasite/webiscite/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from django.conf import settings
from django.db import models
from django.urls import reverse
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django_celery_beat.models import PeriodicTask
from model_utils.models import TimeStampedModel
Expand Down Expand Up @@ -32,6 +33,9 @@ class PullRequest(TimeStampedModel):
diff_url = models.URLField(help_text=_("URL to the diff of the pull request"))
# Store Github username of author even if they are not a user on the site
author_name = models.CharField(max_length=100)
draft = models.BooleanField(
default=False, help_text=_("Whether the pull request is a draft on GitHub")
)
#:
status = models.CharField(
max_length=6,
Expand Down Expand Up @@ -72,7 +76,9 @@ def close(self) -> "Bill | None":
self.save()

try:
bill: Bill = self.bill_set.get(status=Bill.Status.OPEN)
bill: Bill = self.bill_set.get(
status__in=[Bill.Status.OPEN, Bill.Status.DRAFT]
)
except Bill.DoesNotExist:
self.log("No open bill found")
return None
Expand Down Expand Up @@ -119,6 +125,7 @@ class Status(models.TextChoices):

:meta private:"""

DRAFT = "draft", _("Draft")
OPEN = "open", _("Open")
APPROVED = "approved", _("Approved")
REJECTED = "rejected", _("Rejected")
Expand Down Expand Up @@ -151,10 +158,10 @@ class Meta:
models.UniqueConstraint(
fields=("pull_request",),
# Can't reference Bill.Status because Bill isn't defined yet
condition=models.Q(status="open"),
name="unique_open_pull_request",
condition=models.Q(status__in=["open", "draft"]),
name="unique_active_pull_request",
violation_error_message=_(
"A Bill for this pull request is already open"
"A Bill for this pull request is already active"
),
),
]
Expand Down Expand Up @@ -247,6 +254,19 @@ def close(self) -> None:
self._submit_task.save()
self.log("Submit task disabled")

def publish(self) -> None:
"""Transition a draft bill to open, enabling voting and scheduling submission"""
if self.status != self.Status.DRAFT:
raise ValueError("Only draft bills can be published")

self.status = self.Status.OPEN
self.save()

self._submit_task.enabled = True
self._submit_task.last_run_at = timezone.now()
self._submit_task.save()
self.log("Published")

def submit(self) -> None:
"""Check if the bill has enough votes to pass and update the status"""
# Bill was closed before voting period ended
Expand Down
2 changes: 2 additions & 0 deletions democrasite/webiscite/tests/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class PullRequestFactory(factory.django.DjangoModelFactory[PullRequest]):
title = factory.Faker("text", max_nb_chars=50)
author_name = factory.Faker("user_name")
status = "open"
draft = False
additions = factory.Faker("random_int")
deletions = factory.Faker("random_int")
sha = factory.Faker("pystr", min_chars=40, max_chars=40)
Expand Down Expand Up @@ -67,6 +68,7 @@ class GithubPullRequestFactory(factory.Factory[dict[str, Any]]):
state = "open"
additions = factory.SelfAttribute("bill.pull_request.additions")
deletions = factory.SelfAttribute("bill.pull_request.deletions")
draft = False
diff_url = "" # Keep blank so request.get raises an error

class Meta:
Expand Down
Loading