);
};
- 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();