Skip to content

Question aggregates by location level, quick report aggregates and polling station counts - #1127

Merged
idormenco merged 5 commits into
mainfrom
feat/question-location-aggregates
Oct 20, 2026
Merged

idormenco merged 5 commits into
mainfrom
feat/question-location-aggregates

Conversation

@idormenco

Copy link
Copy Markdown
Contributor

What changes

Question aggregates by location level

  • One question's answers for form submissions, incident reports and citizen reports, broken down by the locations of one level (…QuestionAggregateByLevel functions).

Quick reports aggregates

  • Overall and per location.

Polling stations per option

  • Single select, multi select and rating aggregates fill pollingStationsHistogram in memory too (V1), as the database aggregation already does.

Polling stations per question

  • Every answer aggregate now has pollingStationsCount: the distinct polling stations of the submissions that answered the question. Citizen reports get locationsCount instead.
  • Both are set overall, in each locationAggregates entry, and in the by-level aggregates, for V1 and V2.
  • A place's own pollingStationsCount counts the stations of all its submissions, including ones that skipped the question. The web app uses the new count to show "63 answers in 29 polling stations" per place.
  • Migration AddQuestionAggregatePlaceCounts recreates "AggregateAnswers" with the two counts. Its Down restores the previous definition.

Fix

  • Sorting monitoring observers by status and by observer name.

Testing

  • dotnet test tests/Module.Answers.UnitTests: 112 of 112 pass. New tests cover distinct counting, null when no answer has a station or location, and reading the counts back from database JSON.
  • dotnet test tests/Vote.Monitor.Api.IntegrationTests --filter GetAggregatedTests: 5 of 5 pass on Testcontainers Postgres with every migration applied.
  • "AggregateAnswers" was also called directly on sample data:
    • A question answered twice at one station and once at another gives 2.
    • An unanswered question gives 0.
    • Citizen reports return only LocationsCount.

Not covered

  • No integration tests exist for the V2 or by-level endpoints.
  • The migration's Down step was not run.
  • The migration's Designer file was copied from the previous migration with only the id and class name changed. The installed dotnet ef (7.0) is older than the project's EF 8.

ion.dormenco added 5 commits October 20, 2026 12:06
…ent and citizen reports

- New :aggregatedByLevel endpoints for form submissions, incident reports and
  citizen reports aggregate the answers to one question, overall and per
  distinct Level1..<level> location path. They return 404 when the form is not
  available to the ngo or does not contain the question.
- The level is a new LocationLevel smart enum (Level1..Level5).
- The aggregation runs in the database: the shared "AggregateQuestionByLevel"
  function and one entry function per report type.
- quick-reports:aggregated returns the totals and every Level1..Level5 location
  (with Id, Level and ParentId); quick-reports:aggregatedByLevel returns the
  locations of one level.
- Each entry counts quick reports, polling stations and observers, with
  histograms per incident category, location type and follow-up status (every
  value listed, 0 when not reported).
The list ignored SortColumnName=status and observerName (sent by the web app),
and its fallback sort matched no column, so rows came back unordered. Rows with
the same sort value are now ordered by display name and id, so pages do not
overlap.
Single select, multi select and rating aggregates computed in memory (V1
aggregated endpoints) now fill PollingStationsHistogram with the number of
distinct polling stations where each option / value was picked, as the database
aggregation does. It is not set when the submissions have no polling station.
Every answer aggregate (all question types, overall and in each
LocationAggregates entry, V1 and V2 and the by-level aggregates) now has
PollingStationsCount: the distinct polling stations of the submissions that
answered the question, and, for citizen reports, LocationsCount: their
distinct locations. A place's PollingStationsCount counts the stations of all
its submissions, including ones that skipped the question.

- In memory: BaseAnswerAggregate tracks the distinct station / location ids it
  aggregates (null when none had one); citizen reports pass their location id.
- Database: migration AddQuestionAggregatePlaceCounts recreates
  "AggregateAnswers" with the two counts (0 for an unanswered question); Down
  restores the previous definition.
@vercel

vercel Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
votemonitor Ready Ready Preview Oct 20, 2026 10:35am UTC

Request Review

@idormenco
idormenco merged commit efc44d4 into main Oct 20, 2026
4 checks passed
@idormenco
idormenco deleted the feat/question-location-aggregates branch October 20, 2026 10:37
public void Aggregate(Guid submissionId, Guid monitoringObserverId, BaseAnswer answer)
/// <param name="pollingStationId">The polling station of the submission; not set for citizen reports.</param>
/// <param name="locationId">The location of the citizen report; only set for citizen reports.</param>
public void Aggregate(Guid submissionId, Guid monitoringObserverId, BaseAnswer answer, Guid? pollingStationId = null, Guid? locationId = null)
public void Aggregate(Guid submissionId, Guid monitoringObserverId, BaseAnswerModel answer)
/// <param name="pollingStationId">The polling station of the submission; not set for citizen reports.</param>
/// <param name="locationId">The location of the citizen report; only set for citizen reports.</param>
public void Aggregate(Guid submissionId, Guid monitoringObserverId, BaseAnswerModel answer, Guid? pollingStationId = null, Guid? locationId = null)
private readonly Dictionary<TKey, HashSet<Guid>> _pollingStations;

/// <param name="keys">The options / values, so that the ones never picked are counted as 0.</param>
public DistinctPollingStations(IEnumerable<TKey> keys)

This branch was successfully deployed

1 active deployment
Preview — ede2a31d Deployed Oct 20, 2026 by vercel[bot]
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