Skip to content

feat(api): table queries for use cases, collaboratives, AI models, publications - #254

Merged
anantjain341 merged 2 commits into
CivicDataLab:devfrom
anantjain341:feat/listing-tables
Oct 22, 2026
Merged

anantjain341 merged 2 commits into
CivicDataLab:devfrom
anantjain341:feat/listing-tables

Conversation

@anantjain341

@anantjain341 anantjain341 commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Backend for "Pagination and filter implementation" (https://github.com/CivicDataLab/DataSpace/issues/277): use cases (https://github.com/CivicDataLab/DataSpace/issues/278), collaboratives (https://github.com/CivicDataLab/DataSpace/issues/279), AI models (https://github.com/CivicDataLab/DataSpace/issues/280) and publications (https://github.com/CivicDataLab/DataSpace/issues/287).

Adds four new queries for the dashboard tables, built the same way as datasetsTable (#215):
useCasesTable, collaborativesTable, aiModelsTable, publicationsTable.
The existing useCases, collaboratives, aiModels and publications queries are untouched, so nothing that runs today changes.

What you get in one call

A page of rows, the total count, and the per-status numbers for the tabs. All four look the same:

useCasesTable(filters, sortOptions, limit, offset, includePublic) {
  totalItemsCount                     # total after filters, for "Showing 1–20 of 57"
  statusCounts { status count }       # for the tab labels: Drafts (3), Published (6)
  data { ...any use case field... }
}

data rows are the same types the old queries return (TypeUseCase, TypeCollaborative, TypeAIModel, TypePublication), so select whatever the table shows.

Mapping the current table UI

The four dashboards have the same layout: two tabs and a table.

On screen Send / read
Tabs Drafts / Published a status filter: DRAFT / PUBLISHED. For AI models: REGISTERED (Draft tab) / ACTIVE (Published tab). Numbers for the tab labels come from statusCounts
Title column (use cases, collaboratives, publications) title
Display Name / Type / Provider (AI models) displayName, modelType, provider
Date Created / Date Modified created, modified (AI models: createdAt, updatedAt)
Delete / Unpublish buttons need id (and slug where the link uses it), unchanged
Sorting a column (today fixed to modified, newest first) sortOptions: [{field: "modified", direction: "desc"}] (AI models: updated_at)
A search box, if added filter title with icontains (AI models: display_name)
Showing x–y of z, Previous / Next, if added limit, offset, totalItemsCount

Publications today fetch 100 rows and split the tabs in the browser, so anything past 100 never shows. With publicationsTable the status filter, paging and tab counts all come from the server.

Filters

A list of filters, each: which field, how to compare, what value.
All filters are combined with AND. The value is always a string.

{"field": "title",          "condition": "icontains", "value": "health"}
{"field": "status",         "condition": "in",        "value": "DRAFT,PUBLISHED"}
{"field": "sectors__slug",  "condition": "in",        "value": "health,education"}
{"field": "created",        "condition": "gte",       "value": "2026-01-01"}

Fields you can filter on:

useCasesTable

  • title, status, running_status, id
  • organization, organization__slug
  • sectors__id, sectors__slug, sectors__name
  • geographies__id, geographies__name, geographies__code
  • sdgs__code, sdgs__slug
  • tags__value
  • started_on, completed_on, created, modified (ISO date, e.g. 2026-01-01)

collaborativesTable: the same as use cases, without running_status.

aiModelsTable

  • display_name, name, status, model_type, provider, domain, id
  • organization, organization__slug
  • sectors__id, sectors__slug, sectors__name
  • geographies__id, geographies__name, geographies__code
  • tags__value
  • created_at, updated_at (ISO date)

publicationsTable

  • title, status, license, id
  • resource_type (id), resource_type__slug
  • organization, organization__slug
  • sectors__id, sectors__slug, sectors__name
  • geographies__id, geographies__name, geographies__code
  • publication_date, created, modified (ISO date)

Field names are the model's names in snake_case (display_name, created_at), as in datasetsTable. Filtering on several sectors, geographies or tags never returns a row twice.

Ways to compare:

  • exact (default), iexact — equals
  • contains, icontains — text search (use icontains for a search box)
  • startswith, endswith and their i versions
  • gt, gte, lt, lte — for dates and numbers
  • in — any of a comma-separated list
  • isnull — "true" or "false"

Anything outside this list returns a GraphQL error that names the allowed fields.
Need another column in the filter panel? It is a one-line change on the backend; ask.

Sorting

"sortOptions": [{"field": "modified", "direction": "desc"}]

Fields:

  • useCasesTable: title, created, modified, status, running_status, started_on, completed_on
  • collaborativesTable: title, created, modified, status, started_on, completed_on
  • aiModelsTable: display_name, name, created_at, updated_at, status, model_type, provider
  • publicationsTable: title, created, modified, status, publication_date, download_count

Several keys are allowed. Default when omitted: newest first (created, AI models created_at). The dashboards today sort by last modified, so send that sort to keep the same order.

Pagination

  • limit = page size, 1 to 100 (default 50)
  • offset = rows to skip
  • pages = ceil(totalItemsCount / limit), offset = (page − 1) × limit

statusCounts is computed with every filter applied except the status filter, so the tab numbers do not change when switching tabs.

Who sees what

Same rules as each module's current list query. Send the usual headers: Authorization: Bearer <token> and, inside an organisation, organization: <slug>.

  • In an organisation: that organisation's rows.
  • Logged in, no organisation: your own rows (superuser sees all). For AI models and publications: your own rows that have no organisation.
  • Not logged in: published rows (AI models: public and active ones). This differs from datasetsTable, which returns nothing here; it follows what useCases, collaboratives, aiModels and publications already do.
  • includePublic: true adds every published row (AI models: public and active).

Notes on statuses:

  • Use cases and collaboratives can also be ARCHIVED. The current design has no tab for them, so they show on neither tab, the same as today.
  • AI models have more statuses (DRAFT, VALIDATING, AUDITING, APPROVED, FLAGGED, DEPRECATED). statusCounts lists every status that has rows, so a later Approved tab only needs the filter.

Full example

query UseCasesTable($filters: [FilterSpec!], $sort: [SortSpec!], $limit: Int, $offset: Int) {
  useCasesTable(filters: $filters, sortOptions: $sort, limit: $limit, offset: $offset) {
    totalItemsCount
    statusCounts { status count }
    data { id title slug status created modified }
  }
}

{
  "filters": [{"field": "status", "condition": "exact", "value": "DRAFT"}],
  "sort": [{"field": "modified", "direction": "desc"}],
  "limit": 20,
  "offset": 0
}

AI models, Published tab:

aiModelsTable(
  filters: [{field: "status", value: "ACTIVE"}]
  sortOptions: [{field: "updated_at", direction: "desc"}]
  limit: 20
) {
  totalItemsCount
  statusCounts { status count }
  data { id displayName modelType provider createdAt updatedAt }
}

Errors

Ordinary GraphQL errors, for example:

  • Cannot filter on 'xyz'. Allowed fields: …
  • limit (500) must be between 1 and 100
  • offset (-1) must be 0 or more
  • A value of the wrong type (text for an id or a date) returns Django's validation message, the same as datasetsTable today.

No migration, no new dependency. Helpers live in api/utils/qs_utils.py. The status counting is now one shared helper there (status_counts), used by these four tables and by datasetsTable (second commit; it keeps its DatasetStatusCount type, and the GraphQL schema is unchanged).

…blications

Add useCasesTable, collaborativesTable, aiModelsTable and publicationsTable,
shaped like datasetsTable: client filters and sort options limited to an
allowlist per module, limit/offset paging, the total after filters, and
counts per status for the dashboard tabs (ignoring the status filter so tab
labels stay stable).

Each query shows the same rows as the module's existing list query, plus
published (for AI models: public and active) rows when includePublic is
set. The existing list queries are unchanged. StatusCount and
status_counts() are shared in api/utils/qs_utils.py. No migration.
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

The tab counts in datasetsTable were an inline copy of what
status_counts() in api/utils/qs_utils.py now does for the other four
tables. Call the helper instead, so all five tables count statuses in one
place. status_counts() takes an optional item_type so the dataset table
keeps its DatasetStatusCount type; the GraphQL schema is unchanged.
@anantjain341
anantjain341 marked this pull request as ready for review October 22, 2026 15:09
@anantjain341
anantjain341 merged commit 3143e40 into CivicDataLab:dev Oct 22, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants