diff --git a/client/src/adapter/dosDashboardRefresh.ts b/client/src/adapter/dosDashboardRefresh.ts index 27fadce6..04813fd5 100644 --- a/client/src/adapter/dosDashboardRefresh.ts +++ b/client/src/adapter/dosDashboardRefresh.ts @@ -167,5 +167,6 @@ export function parse(data: any) { publicMeetingDate: parsePublicMeetingDate(data), riskLimit: parseRiskLimit(data), seed: _.get(data, 'audit_info.seed'), + generateAssertionsSummaries: data.generate_assertions_summaries, }; } diff --git a/client/src/component/DOS/DefineAudit/GenerateAssertionsPage.tsx b/client/src/component/DOS/DefineAudit/GenerateAssertionsPage.tsx index d2314ebc..ad38b549 100644 --- a/client/src/component/DOS/DefineAudit/GenerateAssertionsPage.tsx +++ b/client/src/component/DOS/DefineAudit/GenerateAssertionsPage.tsx @@ -1,20 +1,20 @@ -import counties from 'corla/data/counties'; import * as _ from 'lodash'; import * as React from 'react'; -import { Breadcrumb, Button, Card, Intent, Spinner } from '@blueprintjs/core'; +import {Breadcrumb, Button, Card, Intent, Spinner} from '@blueprintjs/core'; import exportAssertionsAsCsv from 'corla/action/dos/exportAssertionsAsCsv'; import exportAssertionsAsJson from 'corla/action/dos/exportAssertionsAsJson'; import generateAssertions from 'corla/action/dos/generateAssertions'; import DOSLayout from 'corla/component/DOSLayout'; -import AssertionStatus = DOS.AssertionStatus; + +const generationTimeoutParam = 'timeLimitSeconds'; const Breadcrumbs = () => ( ); @@ -54,7 +54,7 @@ class GenerateAssertionsPage extends React.Component { alert('generateAssertions error in fetchAction ' + reason); @@ -74,12 +74,12 @@ class GenerateAssertionsPage extends React.Component
- { this.displayTimeoutInput() && + {this.displayTimeoutInput() &&
{ - const {status} = props; + interface CombinationSummary { + combinedData: CombinedData; + } + const CombinedTableRow = (input: CombinationSummary) => { + const {combinedData} = input; + + // Succeeded and retry can be undefined - if so this leaves the space blank. + // {combinedData.succeeded != undefined ? {combinedData.succeeded ? 'Success' : 'Failure'} : ''} + let successString = combinedData.succeeded ? 'Success' : 'Failure'; + let retryString = combinedData.retry ? 'Yes' : 'No'; return ( - { status.contestName } - - { status.succeeded ? 'Success' : 'Failure' } + {combinedData.contestName} + + {combinedData.succeeded == undefined ? '' : successString} - {status.retry ? 'Yes' : 'No'} + {combinedData.retry == undefined ? '' : retryString} + {combinedData.winner} + {combinedData.error} + {combinedData.warning} + {combinedData.message} ); }; - const assertionRows = _.map(this.props.dosState.assertionGenerationStatuses, a => ( - - )); + // Make a CombinedData structure out of a GenerateAssertionsSummary by filling in blank status. + const fillBlankStatus = (s: DOS.GenerateAssertionsSummary): CombinedData => { + return { + contestName: s.contestName, + succeeded: undefined, + retry: undefined, + winner: s.winner, + error: s.error, + warning: s.warning, + message: s.message + }; + } + + // Make a CombinedData structure out of an AssertionsStatus by filling in blank summary data. + const fillBlankSummary = (s: DOS.AssertionStatus): CombinedData => { + return { + contestName: s.contestName, + succeeded: s.succeeded, + retry: s.retry, + winner: '', + error: '', + warning: '', + message: '' + }; + } + + // Make a CombinedData structure out of an AssertionsStatus and a GenerateAssertionsSummary. + const combineSummaryAndStatus = (s: DOS.AssertionStatus, t: DOS.GenerateAssertionsSummary): CombinedData => { + return { + contestName: s.contestName, + succeeded: s.succeeded, + retry: s.retry, + winner: t.winner, + error: t.error, + warning: t.warning, + message: t.message + }; + } + + // Join up the rows by contest name if matching. If there is no matching contest name in the other + // list, add a row with blanks for the missing data. + // Various kinds of absences are possible, because there may be empty summaries at the start; + // conversely, in later phases we may rerun generation (and hence get status) for only a few contests. + const joinRows = (statuses: DOS.AssertionGenerationStatuses | undefined, summaries: DOS.GenerateAssertionsSummary[]) => { + summaries.sort((a, b) => a.contestName < b.contestName ? -1 : 1); + let rows: CombinedData[] = []; + let i = 0, j = 0; + + if (statuses === undefined) { + // No status yet. Just print summary data. + return summaries.map(s => { + return fillBlankStatus(s); + }) + + } else { + // Iterate along the two sorted lists at once, combining them if the contest name matches, and + // filling in blank data otherwise. + statuses.sort((a, b) => a.contestName < b.contestName ? -1 : 1); + while (i < statuses.length && j < summaries.length) { + if (statuses[i].contestName === summaries[j].contestName) { + // Matching contest names. Join the rows and move indices along both lists. + rows.push(combineSummaryAndStatus(statuses[i++], summaries[j++])); + } else if (statuses[i].contestName < summaries[j].contestName) { + // We have a status with no matching summary. Fill in the summary with blanks. + // increment status index only. + rows.push(fillBlankSummary(statuses[i++])); + } else if (statuses[i].contestName < summaries[j].contestName) { + // We have a summary with no matching status. Fill in status 'undefined'. + // Increment summary index only. + rows.push(fillBlankStatus(summaries[j++])); + } + } + while (i < statuses.length) { + // We ran out of summaries. Fill in the rest of the statuses with summary blanks. + rows.push(fillBlankSummary(statuses[i++])); + } + while (j < summaries.length) { + // We ran out of statuses. Fill in the rest of the summaries with status blanks. + rows.push(fillBlankStatus(summaries[j++])); + } + } + return rows; + } + + const combinedRows = _.map(joinRows(this.props.dosState.assertionGenerationStatuses, this.props.dosState.generateAssertionsSummaries), d => ( + + )) if (this.state.generatingAssertions) { return ( - +
Generating Assertions...
); - } else if (this.props.dosState.assertionGenerationStatuses) { + } else if (this.props.dosState.assertionGenerationStatuses || this.props.dosState.generateAssertionsSummaries.length > 0) { return (
@@ -163,16 +266,21 @@ class GenerateAssertionsPage extends React.ComponentContest Assertion Generation Status Advise Retry + Winner + Error + Warning + Message - { assertionRows } + {combinedRows}
); } else { - return
; + return
; } } + } export default GenerateAssertionsPage; diff --git a/client/src/reducer/defaultState.ts b/client/src/reducer/defaultState.ts index ec2c56bc..5b5fb18b 100644 --- a/client/src/reducer/defaultState.ts +++ b/client/src/reducer/defaultState.ts @@ -31,6 +31,7 @@ export function dosState(): DOS.AppState { contests: {}, countyStatus: {}, type: 'DOS', + generateAssertionsSummaries: [], }; } diff --git a/client/src/reducer/dos/dashboardRefreshOk.ts b/client/src/reducer/dos/dashboardRefreshOk.ts index adab4063..55590256 100644 --- a/client/src/reducer/dos/dashboardRefreshOk.ts +++ b/client/src/reducer/dos/dashboardRefreshOk.ts @@ -11,6 +11,7 @@ export default function dashboardRefreshOk( const nextState = merge({}, state, newState); nextState.auditedContests = newState.auditedContests; nextState.countyStatus = newState.countyStatus; + nextState.generateAssertionsSummaries = newState.generateAssertionsSummaries; return nextState; } diff --git a/client/src/saga/dos/sync.ts b/client/src/saga/dos/sync.ts index b79f15cf..85c8283e 100644 --- a/client/src/saga/dos/sync.ts +++ b/client/src/saga/dos/sync.ts @@ -38,6 +38,10 @@ const dashboardPollSaga = createPollSaga( () => DOS_POLL_DELAY, ); +function* generateAssertionsSaga() { + yield takeLatest('DOS_GENERATE_ASSERTIONS_SYNC', () => dashboardRefresh()); +} + function* defineAuditSaga() { yield takeLatest('DOS_DEFINE_AUDIT_SYNC', () => dashboardRefresh()); } @@ -64,6 +68,7 @@ export default function* pollSaga() { countyOverviewSaga(), dashboardPollSaga(), defineAuditSaga(), + generateAssertionsSaga(), defineAuditReviewSaga(), randomSeedSaga(), selectContestsPollSaga(), diff --git a/client/src/types/dos.d.ts b/client/src/types/dos.d.ts index 38d9abee..206a8210 100644 --- a/client/src/types/dos.d.ts +++ b/client/src/types/dos.d.ts @@ -17,6 +17,7 @@ declare namespace DOS { standardizingContests?: boolean; generatingAssertions?: boolean; assertionsGenerated?: boolean; + generateAssertionsSummaries: DOS.GenerateAssertionsSummary[]; assertionGenerationStatuses?: DOS.AssertionGenerationStatuses; type: 'DOS'; } @@ -49,6 +50,15 @@ declare namespace DOS { [type: string]: number; } + interface GenerateAssertionsSummary { + id: number; + contestName: string; + winner: string; + error: string; + warning: string; + message: string; + } + interface CanonicalContests { [countyId: string]: string[]; } diff --git a/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/GenerateAssertionsSummary.java b/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/GenerateAssertionsSummary.java index 6b3a39d1..ddc2671b 100644 --- a/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/GenerateAssertionsSummary.java +++ b/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/GenerateAssertionsSummary.java @@ -21,11 +21,18 @@ package au.org.democracydevelopers.corla.model; +import au.org.democracydevelopers.corla.query.GenerateAssertionsSummaryQueries; import org.apache.log4j.LogManager; import org.apache.log4j.Logger; +import us.freeandfair.corla.persistence.Persistence; +import us.freeandfair.corla.persistence.PersistentEntity; import javax.persistence.*; +import java.util.List; +import java.util.SortedMap; +import java.util.TreeMap; + import static java.util.Collections.min; @@ -37,7 +44,7 @@ */ @Entity @Table(name = "generate_assertions_summary") -public class GenerateAssertionsSummary { +public class GenerateAssertionsSummary implements PersistentEntity { /** * Class-wide logger. @@ -100,6 +107,13 @@ public class GenerateAssertionsSummary { public GenerateAssertionsSummary() { } + /** + * @return the contest name. + */ + public String getContestName() { + return contestName; + } + /** * @return the winner. */ @@ -127,4 +141,19 @@ public String getWarning() { public String getMessage() { return message; } + + @Override + public Long id() { + return id; + } + + @Override + public void setID(Long the_id) { + id = the_id; + } + + @Override + public Long version() { + return version; + } } \ No newline at end of file diff --git a/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/query/GenerateAssertionsSummaryQueries.java b/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/query/GenerateAssertionsSummaryQueries.java index 6cb71ca0..210ff163 100644 --- a/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/query/GenerateAssertionsSummaryQueries.java +++ b/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/query/GenerateAssertionsSummaryQueries.java @@ -30,6 +30,7 @@ import javax.persistence.TypedQuery; import java.util.List; import java.util.Optional; +import java.util.SortedMap; import static au.org.democracydevelopers.corla.endpoint.GenerateAssertions.UNKNOWN_WINNER; @@ -102,5 +103,4 @@ public static Optional matching(final String contestN throw new RuntimeException(msg); } } - } diff --git a/server/eclipse-project/src/main/java/us/freeandfair/corla/json/DoSDashboardRefreshResponse.java b/server/eclipse-project/src/main/java/us/freeandfair/corla/json/DoSDashboardRefreshResponse.java index bcfaf7ad..389a7775 100644 --- a/server/eclipse-project/src/main/java/us/freeandfair/corla/json/DoSDashboardRefreshResponse.java +++ b/server/eclipse-project/src/main/java/us/freeandfair/corla/json/DoSDashboardRefreshResponse.java @@ -26,6 +26,7 @@ import javax.persistence.PersistenceException; +import au.org.democracydevelopers.corla.model.GenerateAssertionsSummary; import org.apache.log4j.LogManager; import org.apache.log4j.Logger; @@ -111,20 +112,27 @@ public class DoSDashboardRefreshResponse { */ private final SortedMap my_audit_types; + /** + * The generate assertions summaries, for IRV contests. Keyed by contest name (which is repeated + * in the GenerateAssertionsSummary). + */ + private final List my_generate_assertions_summaries; + /** * Constructs a new DosDashboardRefreshResponse. * - * @param the_asm_state The ASM state. - * @param the_audited_contests The audited contests. - * @param the_estimated_ballots_to_audit The estimated ballots to audit, by - * contest. - * @param the_optimistic_ballots_to_audit The optimistic ballots to audit, by - * contest. - * @param the_discrepancy_count The discrepancy count for each discrepancy - * type, by contest. - * @param the_county_status The county statuses. - * @param the_hand_count_contests The hand count contests. - * @param the_audit_info The election info. + * @param the_asm_state The ASM state. + * @param the_audited_contests The audited contests. + * @param the_estimated_ballots_to_audit The estimated ballots to audit, by contest. + * @param the_optimistic_ballots_to_audit The optimistic ballots to audit, by contest. + * @param the_discrepancy_counts The discrepancy count for each discrepancy + * type, by contest. + * @param the_county_status The county statuses. + * @param the_hand_count_contests The hand count contests. + * @param the_audit_info The election info. + * @param the_audit_reasons The reasons for auditing each contest. + * @param the_audit_types The audit type (usually either COMPARISON or NOT_AUDITABLE) + * @param the_generate_assertions_summaries The GenerateAssertionsSummaries, for IRV contests. */ @SuppressWarnings("PMD.ExcessiveParameterList") protected DoSDashboardRefreshResponse(final ASMState the_asm_state, @@ -136,7 +144,8 @@ protected DoSDashboardRefreshResponse(final ASMState the_asm_state, final List the_hand_count_contests, final AuditInfo the_audit_info, final SortedMap the_audit_reasons, - final SortedMap the_audit_types) { + final SortedMap the_audit_types, + final List the_generate_assertions_summaries) { my_asm_state = the_asm_state; my_audited_contests = the_audited_contests; my_estimated_ballots_to_audit = the_estimated_ballots_to_audit; @@ -147,6 +156,7 @@ protected DoSDashboardRefreshResponse(final ASMState the_asm_state, my_audit_info = the_audit_info; my_audit_reasons = the_audit_reasons; my_audit_types = the_audit_types; + my_generate_assertions_summaries = the_generate_assertions_summaries; } /** @@ -223,11 +233,17 @@ public static DoSDashboardRefreshResponse createResponse(final DoSDashboard dash final DoSDashboardASM asm = ASMUtilities.asmFor(DoSDashboardASM.class, DoSDashboardASM.IDENTITY); + // Load all the Generate Assertions Summaries from the database into the generate_assertions_list. + final List generate_assertions_list + = Persistence.getAll(GenerateAssertionsSummary.class); + + return new DoSDashboardRefreshResponse(asm.currentState(), audited_contests, estimated_ballots_to_audit, optimistic_ballots_to_audit, discrepancy_count, countyStatusMap(), hand_count_contests, - dashboard.auditInfo(), audit_reasons, audit_types); + dashboard.auditInfo(), audit_reasons, audit_types, + generate_assertions_list); } /** diff --git a/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/endpoint/GenerateAssertionsAPITests.java b/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/endpoint/GenerateAssertionsAPITests.java index 0bc717ef..a8ca6951 100644 --- a/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/endpoint/GenerateAssertionsAPITests.java +++ b/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/endpoint/GenerateAssertionsAPITests.java @@ -36,7 +36,6 @@ import org.mockito.MockitoAnnotations; import org.testcontainers.containers.PostgreSQLContainer; import org.testcontainers.ext.ScriptUtils; -import org.testcontainers.jdbc.JdbcDatabaseDelegate; import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; @@ -86,13 +85,13 @@ public class GenerateAssertionsAPITests extends TestClassWithAuth { private final GenerateAssertions endpoint = new GenerateAssertions(); /** - * Mock response for tinyExample1 contest + * Mock response for tinyExample1 contest, which succeeded and is not worth retrying. */ private final static GenerateAssertionsResponse tinyIRVResponse = new GenerateAssertionsResponse(tinyIRV, true, false); /** - * Request for tinyExample1 contest + * Request for tinyExample1 contest, intended as a boring request with normal parameters. */ private final static GenerateAssertionsRequest tinyIRVRequest = new GenerateAssertionsRequest(tinyIRV, tinyIRVCount, 5, @@ -130,7 +129,8 @@ public class GenerateAssertionsAPITests extends TestClassWithAuth { */ @BeforeClass public static void beforeAll() { - var containerDelegate = setupContainerStartPostgres(postgres); + + final var containerDelegate = setupContainerStartPostgres(postgres); var s = Persistence.openSession(); s.beginTransaction();