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
116 changes: 116 additions & 0 deletions .claude/skills/docs/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
---
name: docs
description: Update, review, or build the Sphinx documentation for Democrasite
disable-model-invocation: true
argument-hint: "[what to update or check, e.g. 'review for accuracy' or 'add activitypub section']"
---

## Task
$ARGUMENTS

## Documentation structure

```
docs/
├── conf.py — Sphinx config (theme, extensions, Django setup)
├── index.rst — Master toctree (README, CONTRIBUTING, howto, users, webiscite, api/)
├── README.rst — Project overview (mirrors root README)
├── CONTRIBUTING.rst — Dev setup and contribution guide
├── howto.rst — Instructions for building and writing docs
├── users.rst — Users app overview (minimal; relies on autodoc)
├── webiscite.rst — Core app narrative (pipeline, bill lifecycle)
└── api/ — Auto-generated RST files (do not edit manually)
├── democrasite.rst
├── democrasite.webiscite.rst
├── democrasite.webiscite.models.rst
├── democrasite.webiscite.managers.rst
├── democrasite.webiscite.tasks.rst
├── democrasite.webiscite.webhooks.rst
├── democrasite.webiscite.constitution.rst
├── democrasite.webiscite.views.rst
├── democrasite.webiscite.api.rst
├── democrasite.users.rst
├── democrasite.activitypub.rst
└── … (one file per module)
```

## Key make commands

All run from the `docs/` directory (or with `make -C docs <target>`):

| Command | What it does |
|---|---|
| `make apidocs` | Regenerate all `docs/api/*.rst` from source using sphinx-apidoc. Run this whenever Python modules are added or removed. |
| `make livehtml` | Build docs and serve with live reload at http://localhost:9000. Requires the docs Docker container (`docker compose -f docker-compose.docs.yml up`). |
| `make clean` | Remove `_build/` and `api/` directories for a fresh build. |
| `make html` | One-off HTML build into `_build/html/`. |

To serve with live reload locally (in Docker):
```
docker compose -f docker-compose.docs.yml up
```

To regenerate API docs locally (sphinx-apidoc must be installed):
```
make -C docs apidocs
```

## What is auto-generated vs. manual

- **`docs/api/`** — fully auto-generated by `make apidocs` (sphinx-apidoc). Never edit these files directly; edit the Python docstrings instead, then re-run `make apidocs`.
- **`docs/webiscite.rst`** and **`docs/users.rst`** — manually maintained narrative docs. Keep these in sync with code changes (especially `webiscite.rst` for the PR pipeline and bill lifecycle).
- **`docs/CONTRIBUTING.rst`** and **`docs/howto.rst`** — manually maintained.

## Docstring style

Use Google-style docstrings (supported via the Napoleon extension). Example:

```python
def my_function(arg: int) -> str:
"""One-line summary.

Longer description if needed.

Args:
arg: Description of the argument.

Returns:
Description of the return value.

Raises:
ValueError: When and why.
"""
```

Line length: 88 characters (enforced by ruff). Wrap long docstring lines to stay within this limit.

## Steps for common tasks

### Reviewing docs for accuracy
1. Read `docs/webiscite.rst` and cross-check function/class references against actual source code
2. Check that docstrings in `democrasite/webiscite/managers.py`, `models.py`, `tasks.py`, `webhooks.py`, and `constitution.py` have correct parameter names and return descriptions
3. Run `make apidocs` if modules have been added or removed since the last regeneration
4. Run `just lint` to confirm no ruff line-length violations were introduced

### Adding or updating narrative docs
1. Edit the relevant `.rst` file in `docs/` (not under `docs/api/`)
2. Use Sphinx cross-references for code identifiers:
- `:class:\`~democrasite.webiscite.models.Bill\`` — class link
- `:func:\`~democrasite.webiscite.tasks.submit_bill\`` — function link
- `:meth:\`~democrasite.webiscite.webhooks.PullRequestHandler.opened\`` — method link
3. Verify the build: `docker compose -f docker-compose.docs.yml up`

### Adding a new Python module
1. Write a module-level docstring at the top of the file
2. Run `make apidocs` to regenerate `docs/api/` — this creates the new RST file and adds it to the relevant package toctree automatically
3. Commit the updated `docs/api/` files alongside the new module

### Updating docs after refactoring
1. Update any cross-references in `docs/webiscite.rst` or `docs/users.rst` that point to renamed/removed identifiers
2. Update affected docstrings in source files
3. Run `make apidocs` if the module structure changed (files added/removed)
4. Run `just lint` to verify no line-length violations

## Published docs
- ReadTheDocs URL: https://cookiestocracy.readthedocs.io/en/latest/
- Builds automatically on push to the main branch
2 changes: 2 additions & 0 deletions democrasite/activitypub/models.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Models for the activitypub app."""

from django.contrib.auth import get_user_model
from django.db import models
from django.urls import reverse
Expand Down
28 changes: 22 additions & 6 deletions democrasite/webiscite/managers.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Managers for the webiscite app models."""

from logging import WARNING
from typing import TYPE_CHECKING
from typing import Any
Expand All @@ -16,16 +18,14 @@

class PullRequestManager[T](models.Manager):
def create_from_github(self, pr: dict[str, Any]) -> T:
"""Create a :class:`~democrasite.webiscite.models.PullRequest` and optionally a
:class:`~democrasite.webiscite.models.Bill` instance from a GitHub pull request
"""Create or update a :class:`~democrasite.webiscite.models.PullRequest` from
a GitHub pull request payload

Args:
pr_args: The parameters for the ``PullRequest``
bill_args: The parameters for the ``Bill`` or None
pr: The pull request data from the GitHub API

Returns:
A tuple containing the new or updated pull request and new bill instance, if
applicable
The new or updated pull request instance
"""
pull_request, created = self.update_or_create(
number=pr["number"],
Expand All @@ -47,6 +47,12 @@ def create_from_github(self, pr: dict[str, Any]) -> T:

class BillManager[T](models.Manager):
def get_queryset(self):
"""Return a queryset with pull_request pre-fetched and vote percentages added.

All Bill querysets include ``total_votes``, ``yes_percent``, and
``no_percent``
annotations, and are ordered by creation date.
"""
return (
super()
.get_queryset()
Expand Down Expand Up @@ -80,6 +86,16 @@ def get_queryset(self):
def annotate_user_vote(
self, user: User, queryset: models.QuerySet["Bill"] | None = None
):
"""Annotate a queryset with the given user's vote on each bill.

Args:
user: The user whose vote to annotate
queryset: The queryset to annotate; defaults to ``self.get_queryset()``

Returns:
The queryset annotated with a ``user_vote`` field
(``True``/``False``/``None``)
"""
if queryset is None:
queryset = self.get_queryset()

Expand Down
13 changes: 7 additions & 6 deletions democrasite/webiscite/webhooks.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Views for processing webhooks.

Each service that sends webhooks should have its own function-based view."""
Each service that sends webhooks should have its own class-based view."""

import hmac
import json
Expand Down Expand Up @@ -151,10 +151,10 @@ def _validate_header(headers: request.HttpHeaders) -> HttpResponseBadRequest | N
"""Validate the headers of a request from a webhook

Args:
headers (dict): The headers from the request to validate
headers: The headers from the request to validate

Returns:
str: Error message if the headers are invalid, otherwise an empty string
A bad request response if required headers are missing, otherwise ``None``
"""
header_signature = headers.get("x-hub-signature-256")
if header_signature is None:
Expand All @@ -177,11 +177,12 @@ def _validate_signature(
"""Validate the signature of a request from a webhook

Args:
header_signature (str): The signature from the request to validate
request_body (bytes): The body of the request to validate
header_signature: The HMAC signature from the request headers
request_body: The raw body of the request to validate

Returns:
str: Error message if the signature is invalid, otherwise an empty string
An error response if the signature is invalid or uses an unsupported
digest, otherwise ``None``
"""
digest_name, signature = header_signature.split("=")
if digest_name != "sha256":
Expand Down
28 changes: 16 additions & 12 deletions docs/webiscite.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,27 @@ Pull Requests
Pull Request Processing Pipeline
--------------------------------

When a pull request is created on `GitHub`_, a `webhook`_ makes a request to
the GitHub :func:`webhook view <democrasite.webiscite.webhooks.github_hook>`.
When a pull request is created on `GitHub`_, a `webhook`_ makes a POST request
to the :class:`~democrasite.webiscite.webhooks.GithubWebhookView`, which
validates the request signature and dispatches to the appropriate handler.

This method parses the data from the request and calls
:func:`~democrasite.webiscite.tasks.process_pull`
to handle the request. In the event a pull request was opened or reopened,
:func:`~democrasite.webiscite.tasks.pr_opened` is called.
For pull request events, the request is handled by
:class:`~democrasite.webiscite.webhooks.PullRequestHandler`. When a pull
request is opened or reopened, its
:meth:`~democrasite.webiscite.webhooks.PullRequestHandler.opened` method
creates a :class:`~democrasite.webiscite.models.PullRequest` instance.

If the user who created the pull request has a democrasite account, a new
:class:`~democrasite.webiscite.models.Bill`
is created with the information from the pull request and made visible on the
homepage immediately.

A task to execute :func:`~democrasite.webiscite.tasks.submit_bill`
is also put in the celery queue to execute once the voting period ends. The
function verifies that the pull request is open and unedited and then counts
the votes for and against that Bill.
homepage immediately. A :class:`~django_celery_beat.models.PeriodicTask` is
also scheduled to execute :func:`~democrasite.webiscite.tasks.submit_bill`
once the voting period ends.

:func:`~democrasite.webiscite.tasks.submit_bill` verifies that the pull
request is still open and that its SHA has not changed since the bill was
created (i.e. the pull request has not been edited), then counts the votes for
and against that Bill.

If the votes for the Bill pass the threshold, the pull request is merged into
the master branch on Github and automatically deployed, officially making it
Expand Down