diff --git a/backend/druks/models.py b/backend/druks/models.py index 6aac13c5..ebcf2885 100644 --- a/backend/druks/models.py +++ b/backend/druks/models.py @@ -134,10 +134,24 @@ async def get_or_none(cls, **fields: object) -> Self | None: return (await db_session().scalars(cls._select_matching(fields))).one_or_none() + @classmethod + async def all(cls) -> list[Self]: + """Every row, in the class's ordering, or in primary key order when it + declares none.""" + return await cls._list_matching({}) + @classmethod async def filter(cls, **fields: object) -> list[Self]: - """The rows whose fields hold these values, in the class's ordering, or in - primary key order when it declares none.""" + """The rows whose fields hold these values, ordered like ``all()``.""" + if fields: + return await cls._list_matching(fields) + raise TypeError( + f"{cls.__name__}.filter() needs a field to match. " + f"Call {cls.__name__}.all() to read every row." + ) + + @classmethod + async def _list_matching(cls, fields: dict[str, object]) -> list[Self]: from druks.db import db_session ordering = cls._get_order_by() or cls.__mapper__.primary_key diff --git a/backend/druks/scaffolding/app_template/package/models.py-tpl b/backend/druks/scaffolding/app_template/package/models.py-tpl index e2f3a084..6377ed52 100644 --- a/backend/druks/scaffolding/app_template/package/models.py-tpl +++ b/backend/druks/scaffolding/app_template/package/models.py-tpl @@ -3,9 +3,9 @@ from druks.db import Model, StoredSubject # SQLAlchemy models for this app — subclass ``Model``. Druks names each table for the # app and the class ("{{ name }}_report" for ``Report``), keeps the app's own migration # history in ``alembic_version_{{ name }}``, and enforces the "{{ name }}_" prefix at boot. -# A model reads by field (``get``, ``get_or_none``, ``filter``) and writes with +# A model reads with ``all``, ``filter``, ``get``, and ``get_or_none`` and writes with # ``create``, ``save``, and ``delete``; ``class Report(Model, ordering=("-created_at",))`` -# sets the order ``filter`` reads in. +# sets the order ``all`` and ``filter`` read in. # One of these models is usually the thing your runs work on — a note, a repo, a # ticket. Subclass ``StoredSubject`` for that one and point a workflow at it with # ``subject = ThatModel``: it also gets an id, created_at, updated_at, a board, and a diff --git a/backend/tests/druks-field_notes/tests/test_models.py b/backend/tests/druks-field_notes/tests/test_models.py index c3d36f36..e5f9e2bc 100644 --- a/backend/tests/druks-field_notes/tests/test_models.py +++ b/backend/tests/druks-field_notes/tests/test_models.py @@ -23,7 +23,10 @@ async def test_rows_read_by_field_in_the_declared_order(druks_db): first = await Note.create(body="the pump ran hot") second = await Note.create(body="the pump ran hot") + assert await Note.all() == [second, first] assert await Note.filter(body="the pump ran hot") == [second, first] + with pytest.raises(TypeError, match="Note.all()"): + await Note.filter() assert await Note.get(id=str(second.id)) == second assert await Note.get_or_none(id="not an id") is None with pytest.raises(ObjectNotFound, match="No note with id 0"): diff --git a/docs/writing-an-app.md b/docs/writing-an-app.md index 07e941ed..6eac5d24 100644 --- a/docs/writing-an-app.md +++ b/docs/writing-an-app.md @@ -1084,6 +1084,7 @@ A model reads and writes by field: report = await Report.create(repo="acme/widgets", status="open") report = await Report.get(id=report_id) report = await Report.get_or_none(repo="acme/widgets") +reports = await Report.all() open_reports = await Report.filter(status="open") report.status = "closed" await report.save() @@ -1094,8 +1095,9 @@ await report.delete() page with an empty state, so a route or page that names a row by id never spells either. `get_or_none` answers None instead. Both expect one row: two raise SQLAlchemy's `MultipleResultsFound`, so back the fields they read with a -unique constraint. `filter` returns the matching rows in primary key order, or in -the order the class declares on its class line, in Django's form: +unique constraint. `all` returns every row and `filter` the rows that match at +least one field, both in primary key order, or in the order the class declares +on its class line, in Django's form: ```python class Report(Model, ordering=("-created_at",)):