From 9f90d2ec93bbf31d345a5d190ce88f8fe2d8cf8e Mon Sep 17 00:00:00 2001 From: Matthew Foster Walsh <15671892+mfosterw@users.noreply.github.com> Date: Sat, 21 Feb 2026 13:46:54 -0700 Subject: [PATCH 1/2] Fix outdated documentation and incorrect docstrings - Update docs/webiscite.rst to reflect current architecture: GithubWebhookView and PullRequestHandler replace the removed github_hook, process_pull, and pr_opened identifiers - Fix managers.py docstrings: correct parameter names and return descriptions for create_from_github on both managers; add missing docstrings to get_queryset and annotate_user_vote; add module docstring - Fix webhooks.py docstrings: update module docstring to class-based view; fix _validate_header and _validate_signature return type descriptions - Add missing module docstring to activitypub/models.py Co-Authored-By: Claude Sonnet 4.6 --- democrasite/activitypub/models.py | 2 ++ democrasite/webiscite/managers.py | 28 ++++++++++++++++++++++------ democrasite/webiscite/webhooks.py | 13 +++++++------ docs/webiscite.rst | 28 ++++++++++++++++------------ 4 files changed, 47 insertions(+), 24 deletions(-) diff --git a/democrasite/activitypub/models.py b/democrasite/activitypub/models.py index 3bf28eda..9a8dc0b2 100644 --- a/democrasite/activitypub/models.py +++ b/democrasite/activitypub/models.py @@ -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 diff --git a/democrasite/webiscite/managers.py b/democrasite/webiscite/managers.py index ee2c9fb0..2b5886ec 100644 --- a/democrasite/webiscite/managers.py +++ b/democrasite/webiscite/managers.py @@ -1,3 +1,5 @@ +"""Managers for the webiscite app models.""" + from logging import WARNING from typing import TYPE_CHECKING from typing import Any @@ -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"], @@ -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() @@ -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() diff --git a/democrasite/webiscite/webhooks.py b/democrasite/webiscite/webhooks.py index 4bcc24f2..62312add 100644 --- a/democrasite/webiscite/webhooks.py +++ b/democrasite/webiscite/webhooks.py @@ -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 @@ -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: @@ -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": diff --git a/docs/webiscite.rst b/docs/webiscite.rst index da405889..73feb61c 100644 --- a/docs/webiscite.rst +++ b/docs/webiscite.rst @@ -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 `. +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 From ec2d11a82464cb40f5df1c273d5c6ab9fffe1e78 Mon Sep 17 00:00:00 2001 From: Matthew Foster Walsh <15671892+mfosterw@users.noreply.github.com> Date: Sun, 22 Feb 2026 20:14:01 -0700 Subject: [PATCH 2/2] Add docs Claude Code skill Co-Authored-By: Claude Sonnet 4.6 --- .claude/skills/docs/SKILL.md | 116 +++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 .claude/skills/docs/SKILL.md diff --git a/.claude/skills/docs/SKILL.md b/.claude/skills/docs/SKILL.md new file mode 100644 index 00000000..efc4c7c0 --- /dev/null +++ b/.claude/skills/docs/SKILL.md @@ -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 `): + +| 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