From 83c7482df7a7ec1de1d3d7fc2bee385697e42323 Mon Sep 17 00:00:00 2001 From: vteague Date: Thu, 12 Dec 2024 22:59:02 +1100 Subject: [PATCH 1/4] Added County name to the Generate Assertions Summary structure that is returned to the client. --- .../DefineAudit/GenerateAssertionsPage.tsx | 38 ++++++---- .../SelectContestsPageContainer.tsx | 4 +- client/src/types/dos.d.ts | 9 ++- .../json/DoSDashboardRefreshResponse.java | 75 +++++++++++++------ .../corla/query/ContestResultQueries.java | 15 ++++ .../src/test/workflows/demo1_defineAudit.http | 11 +++ .../test/workflows/temp/Demo1-test.properties | 2 +- 7 files changed, 110 insertions(+), 44 deletions(-) diff --git a/client/src/component/DOS/DefineAudit/GenerateAssertionsPage.tsx b/client/src/component/DOS/DefineAudit/GenerateAssertionsPage.tsx index 80a9f0cb..b036aa09 100644 --- a/client/src/component/DOS/DefineAudit/GenerateAssertionsPage.tsx +++ b/client/src/component/DOS/DefineAudit/GenerateAssertionsPage.tsx @@ -137,6 +137,7 @@ class GenerateAssertionsPage extends React.Component {combinedData.contestName} + {combinedData.county} {combinedData.succeeded === undefined ? '' : successString} @@ -172,15 +174,16 @@ class GenerateAssertionsPage extends React.Component { + const fillBlankStatus = (s: DOS.GenerateAssertionsSummaryWithCounty): CombinedData => { return { - contestName: s.contestName, - error: s.error, - message: s.message, + contestName: s.summary.contestName, + county: s.countyName, + error: s.summary.error, + message: s.summary.message, retry: undefined, succeeded: undefined, - warning: s.warning, - winner: s.winner, + warning: s.summary.warning, + winner: s.summary.winner, }; }; @@ -188,6 +191,7 @@ class GenerateAssertionsPage extends React.Component { return { contestName: s.contestName, + county: '', error: '', message: '', retry: s.retry, @@ -198,15 +202,16 @@ class GenerateAssertionsPage extends React.Component { + const combineSummaryAndStatus = (s: DOS.AssertionStatus, t: DOS.GenerateAssertionsSummaryWithCounty): CombinedData => { return { contestName: s.contestName, - error: t.error, - message: t.message, + county: t.countyName, + error: t.summary.error, + message: t.summary.message, retry: s.retry, succeeded: s.succeeded, - warning: t.warning, - winner: t.winner, + warning: t.summary.warning, + winner: t.summary.winner, }; }; @@ -215,8 +220,8 @@ class GenerateAssertionsPage extends React.Component { - summaries.sort((a, b) => a.contestName < b.contestName ? -1 : 1); + | undefined, summaries: DOS.GenerateAssertionsSummaryWithCounty[]) => { + summaries.sort((a, b) => a.summary.contestName < b.summary.contestName ? -1 : 1); const rows: CombinedData[] = []; let i = 0; let j = 0; @@ -232,14 +237,14 @@ class GenerateAssertionsPage extends React.Component a.contestName < b.contestName ? -1 : 1); while (i < statuses.length && j < summaries.length) { - if (statuses[i].contestName === summaries[j].contestName) { + if (statuses[i].contestName === summaries[j].summary.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) { + } else if (statuses[i].contestName < summaries[j].summary.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) { + } else if (statuses[i].contestName < summaries[j].summary.contestName) { // We have a summary with no matching status. Fill in status 'undefined'. // Increment summary index only. rows.push(fillBlankStatus(summaries[j++])); @@ -283,6 +288,7 @@ class GenerateAssertionsPage extends React.Component Contest + County Assertion Generation Status Advise Retry Winner diff --git a/client/src/component/DOS/DefineAudit/SelectContestsPageContainer.tsx b/client/src/component/DOS/DefineAudit/SelectContestsPageContainer.tsx index f35556fc..b7fbbeee 100644 --- a/client/src/component/DOS/DefineAudit/SelectContestsPageContainer.tsx +++ b/client/src/component/DOS/DefineAudit/SelectContestsPageContainer.tsx @@ -63,10 +63,10 @@ function mapStateToProps(dosState: DOS.AppState) { if (contest.description === 'IRV') { const assertionSummary = dosState.generateAssertionsSummaries.find( - element => element.contestName === contest.name); + element => element.summary.contestName === contest.name); // Tied IRV contests or contests with failed assertion generations are not auditable - if (assertionSummary && assertionSummary.error.length > 0) { + if (assertionSummary && assertionSummary.summary.error.length > 0) { return false; } } diff --git a/client/src/types/dos.d.ts b/client/src/types/dos.d.ts index 206a8210..93ff68c8 100644 --- a/client/src/types/dos.d.ts +++ b/client/src/types/dos.d.ts @@ -17,7 +17,7 @@ declare namespace DOS { standardizingContests?: boolean; generatingAssertions?: boolean; assertionsGenerated?: boolean; - generateAssertionsSummaries: DOS.GenerateAssertionsSummary[]; + generateAssertionsSummaries: DOS.GenerateAssertionsSummaryWithCounty[]; assertionGenerationStatuses?: DOS.AssertionGenerationStatuses; type: 'DOS'; } @@ -50,6 +50,7 @@ declare namespace DOS { [type: string]: number; } + // Exactly matches the structure of the same name in the server. interface GenerateAssertionsSummary { id: number; contestName: string; @@ -59,6 +60,12 @@ declare namespace DOS { message: string; } + // Exactly matches the record of the same name in the server. + interface GenerateAssertionsSummaryWithCounty { + summary: GenerateAssertionsSummary; + countyName: string; + } + interface CanonicalContests { [countyId: string]: string[]; } 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 389a7775..37148fd6 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 @@ -16,13 +16,7 @@ package us.freeandfair.corla.json; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.SortedMap; -import java.util.TreeMap; +import java.util.*; import javax.persistence.PersistenceException; @@ -33,16 +27,10 @@ import us.freeandfair.corla.asm.ASMState; import us.freeandfair.corla.asm.ASMUtilities; import us.freeandfair.corla.asm.DoSDashboardASM; -import us.freeandfair.corla.model.AuditInfo; -import us.freeandfair.corla.model.AuditReason; -import us.freeandfair.corla.model.AuditType; -import us.freeandfair.corla.model.ContestToAudit; -import us.freeandfair.corla.model.County; -import us.freeandfair.corla.model.ComparisonAudit; -import us.freeandfair.corla.model.CountyDashboard; -import us.freeandfair.corla.model.DoSDashboard; +import us.freeandfair.corla.model.*; import us.freeandfair.corla.persistence.Persistence; import us.freeandfair.corla.query.ComparisonAuditQueries; +import us.freeandfair.corla.query.ContestResultQueries; import us.freeandfair.corla.util.SuppressFBWarnings; /** @@ -116,7 +104,13 @@ public class DoSDashboardRefreshResponse { * The generate assertions summaries, for IRV contests. Keyed by contest name (which is repeated * in the GenerateAssertionsSummary). */ - private final List my_generate_assertions_summaries; + private final List my_generate_assertions_summaries; + + /** + * Placeholder string for when a contest crosses multiple counties. Used for IRV assertion-generation + * summaries. + */ + private final static String MULTIPLE_COUNTIES = "Multiple"; /** * Constructs a new DosDashboardRefreshResponse. @@ -132,7 +126,7 @@ public class DoSDashboardRefreshResponse { * @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. + * @param the_generate_assertions_summaries The GenerateAssertionsSummaries, for IRV contests, with contest names added. */ @SuppressWarnings("PMD.ExcessiveParameterList") protected DoSDashboardRefreshResponse(final ASMState the_asm_state, @@ -145,7 +139,7 @@ protected DoSDashboardRefreshResponse(final ASMState the_asm_state, final AuditInfo the_audit_info, final SortedMap the_audit_reasons, final SortedMap the_audit_types, - final List the_generate_assertions_summaries) { + 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; @@ -233,17 +227,42 @@ 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, - generate_assertions_list); + addCountiesToSummaries()); + } + + + private static List addCountiesToSummaries() { + List generateAssertionsSummaries = new ArrayList<>(); + + // Load all the Generate Assertions Summaries from the database into the generate_assertions_list. + final List generate_assertions_list + = Persistence.getAll(GenerateAssertionsSummary.class); + + // Find out which county each contest is in; fill in 'Multiple' if there is more than one. + for (GenerateAssertionsSummary summary : generate_assertions_list) { + final Optional cr = ContestResultQueries.find(summary.getContestName()); + if (cr.isEmpty() || cr.get().getCounties().isEmpty()) { + // ERROR + } else { + Set counties = cr.get().getCounties(); + if (counties.size() == 1) { + generateAssertionsSummaries.add( + new GenerateAssertionsSummaryWithCounty(summary, counties.stream().findFirst().get().name()) + ); + } else { + // Must be >1 because we already checked for zero. + generateAssertionsSummaries.add( + new GenerateAssertionsSummaryWithCounty(summary, MULTIPLE_COUNTIES) + ); + } + } + } + return generateAssertionsSummaries; } /** @@ -267,4 +286,12 @@ private static SortedMap countyStatusMap() return status_map; } + + /** + * The same as a GenerateAssertionsSummary, but with the county name attached, or "multiple" if more than one. + */ + protected record GenerateAssertionsSummaryWithCounty( + GenerateAssertionsSummary summary, + String countyName + ){}; } diff --git a/server/eclipse-project/src/main/java/us/freeandfair/corla/query/ContestResultQueries.java b/server/eclipse-project/src/main/java/us/freeandfair/corla/query/ContestResultQueries.java index 764bb173..36db50ce 100644 --- a/server/eclipse-project/src/main/java/us/freeandfair/corla/query/ContestResultQueries.java +++ b/server/eclipse-project/src/main/java/us/freeandfair/corla/query/ContestResultQueries.java @@ -37,6 +37,21 @@ public static ContestResult findOrCreate(final String contestName) { } } + /** + * Return an Optional ContestResult, which is Present if a contest of the requested name is in + * the database. + * @param contestName the name of the contest. + * @return an Optional, containing the contest requested by name if present, + * otherwise empty. + */ + public static Optional find(final String contestName) { + final Session s = Persistence.currentSession(); + final Query q = s.createQuery("select cr from ContestResult cr " + + "where cr.contestName = :contestName", ContestResult.class); + q.setParameter("contestName", contestName); + return q.uniqueResultOptional(); + } + /** * Return the ContestResult with the contestName given or create a new * ContestResult with the contestName. diff --git a/server/eclipse-project/src/test/workflows/demo1_defineAudit.http b/server/eclipse-project/src/test/workflows/demo1_defineAudit.http index 60aa4777..f267c4ea 100644 --- a/server/eclipse-project/src/test/workflows/demo1_defineAudit.http +++ b/server/eclipse-project/src/test/workflows/demo1_defineAudit.http @@ -51,6 +51,17 @@ Content-Type: application/json GET http://localhost:8888/generate-assertions?timeLimitSeconds=1 Content-Type: application/x-www-form-urlencoded +> {% + client.test("Request executed successfully", function() { + client.assert(response.status === 200, "Response status is not 200"); + }); + +client.test("Response content-type is json", function() { + var type = response.contentType.mimeType; + client.assert(type === "application/json", "Expected 'application/json' but received '" + type + "'"); + }); +%} + ### Request contests, search for IDs by name. GET http://localhost:8888/contest Content-Type: text/plain diff --git a/server/eclipse-project/src/test/workflows/temp/Demo1-test.properties b/server/eclipse-project/src/test/workflows/temp/Demo1-test.properties index c293502d..4cbc6de7 100644 --- a/server/eclipse-project/src/test/workflows/temp/Demo1-test.properties +++ b/server/eclipse-project/src/test/workflows/temp/Demo1-test.properties @@ -1,5 +1,5 @@ #Ephemeral database config for Demo1 -#Fri Nov 15 15:26:53 AEDT 2024 +#Fri Dec 06 10:45:39 AEDT 2024 raire_url=http\://localhost\:8080 generate_assertions_mock_port=8110 hibernate.driver=org.postgresql.Driver From 94365f217bc65ff3db06743c6662a8631545801b Mon Sep 17 00:00:00 2001 From: vteague Date: Thu, 12 Dec 2024 23:08:40 +1100 Subject: [PATCH 2/4] Add warning if no CR or no county name found; refactor. --- .../corla/json/DoSDashboardRefreshResponse.java | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) 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 37148fd6..2e5c4f5c 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 @@ -246,21 +246,22 @@ private static List addCountiesToSummaries( // Find out which county each contest is in; fill in 'Multiple' if there is more than one. for (GenerateAssertionsSummary summary : generate_assertions_list) { final Optional cr = ContestResultQueries.find(summary.getContestName()); + String countyName = ""; if (cr.isEmpty() || cr.get().getCounties().isEmpty()) { - // ERROR + // This isn't supposed to happen. Keep the summary, with a blank county name, and continue but warn. + LOGGER.warn(String.format("%s %s %s.", "[addCountiesToSummaries] ", "Empty ContestResult or County Name for County ", + summary.getContestName())); } else { Set counties = cr.get().getCounties(); if (counties.size() == 1) { - generateAssertionsSummaries.add( - new GenerateAssertionsSummaryWithCounty(summary, counties.stream().findFirst().get().name()) - ); + countyName = counties.stream().findFirst().get().name(); } else { // Must be >1 because we already checked for zero. - generateAssertionsSummaries.add( - new GenerateAssertionsSummaryWithCounty(summary, MULTIPLE_COUNTIES) - ); + countyName = MULTIPLE_COUNTIES; } } + // Add the summary in, whether we found a county or not. + generateAssertionsSummaries.add(new GenerateAssertionsSummaryWithCounty(summary, countyName)); } return generateAssertionsSummaries; } From 08f94c8c685993e9045b4a01f579328e4aa0acef Mon Sep 17 00:00:00 2001 From: vteague Date: Fri, 20 Dec 2024 10:13:11 +1100 Subject: [PATCH 3/4] Improved commenting based on @michelleblom's review. --- .../freeandfair/corla/json/DoSDashboardRefreshResponse.java | 6 ++++++ 1 file changed, 6 insertions(+) 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 2e5c4f5c..a1530413 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 @@ -236,6 +236,12 @@ public static DoSDashboardRefreshResponse createResponse(final DoSDashboard dash } + /** + * Gets the GenerateAssertionsSummary list from the database, and makes a corresponding list + * with the applicable Counties included in each item. + * @return a list of GenerateAssertionsSummaryWithCounty, which includes the County name if there's + * a unique on for this contest, or "Multiple" if there is more than one. + */ private static List addCountiesToSummaries() { List generateAssertionsSummaries = new ArrayList<>(); From 65c244dd79b23ad28282d081e79f1a6f90406987 Mon Sep 17 00:00:00 2001 From: vteague Date: Fri, 20 Dec 2024 10:14:26 +1100 Subject: [PATCH 4/4] Delete Demo1-test.properties. --- .../src/test/workflows/temp/Demo1-test.properties | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 server/eclipse-project/src/test/workflows/temp/Demo1-test.properties diff --git a/server/eclipse-project/src/test/workflows/temp/Demo1-test.properties b/server/eclipse-project/src/test/workflows/temp/Demo1-test.properties deleted file mode 100644 index 4cbc6de7..00000000 --- a/server/eclipse-project/src/test/workflows/temp/Demo1-test.properties +++ /dev/null @@ -1,11 +0,0 @@ -#Ephemeral database config for Demo1 -#Fri Dec 06 10:45:39 AEDT 2024 -raire_url=http\://localhost\:8080 -generate_assertions_mock_port=8110 -hibernate.driver=org.postgresql.Driver -hibernate.pass=corlasecret -hibernate.dialect=org.hibernate.dialect.PostgreSQL9Dialect -get_assertions_mock_port=8111 -hibernate.show_sql=false -hibernate.user=corlaadmin -hibernate.url=jdbc\:postgresql\://localhost\:32774/corla?loggerLevel\=OFF