Skip to content

Commit

Permalink
Merge pull request #228 from DemocracyDevelopers/221-name-counties-du…
Browse files Browse the repository at this point in the history
…ring-assertion-generation-step

221 name counties during assertion generation step
  • Loading branch information
vteague authored Dec 19, 2024
2 parents 722882d + 65c244d commit ac60bd5
Show file tree
Hide file tree
Showing 6 changed files with 116 additions and 43 deletions.
38 changes: 22 additions & 16 deletions client/src/component/DOS/DefineAudit/GenerateAssertionsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ class GenerateAssertionsPage extends React.Component<GenerateAssertionsPageProps

interface CombinedData {
contestName: string;
county: string;
succeeded: boolean | undefined;
retry: boolean | undefined;
winner: string;
Expand All @@ -159,6 +160,7 @@ class GenerateAssertionsPage extends React.Component<GenerateAssertionsPageProps
return (
<tr>
<td>{combinedData.contestName}</td>
<td>{combinedData.county}</td>
<td style={{color: combinedData.succeeded ? 'green' : 'red'}}>
{combinedData.succeeded === undefined ? '' : successString}
</td>
Expand All @@ -172,22 +174,24 @@ class GenerateAssertionsPage extends React.Component<GenerateAssertionsPageProps
};

// Make a CombinedData structure out of a GenerateAssertionsSummary by filling in blank status.
const fillBlankStatus = (s: DOS.GenerateAssertionsSummary): CombinedData => {
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,
};
};

// Make a CombinedData structure out of an AssertionsStatus by filling in blank summary data.
const fillBlankSummary = (s: DOS.AssertionStatus): CombinedData => {
return {
contestName: s.contestName,
county: '',
error: '',
message: '',
retry: s.retry,
Expand All @@ -198,15 +202,16 @@ class GenerateAssertionsPage extends React.Component<GenerateAssertionsPageProps
};

// Make a CombinedData structure out of an AssertionsStatus and a GenerateAssertionsSummary.
const combineSummaryAndStatus = (s: DOS.AssertionStatus, t: DOS.GenerateAssertionsSummary): CombinedData => {
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,
};
};

Expand All @@ -215,8 +220,8 @@ class GenerateAssertionsPage extends React.Component<GenerateAssertionsPageProps
// 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);
| 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;
Expand All @@ -232,14 +237,14 @@ class GenerateAssertionsPage extends React.Component<GenerateAssertionsPageProps
// 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) {
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++]));
Expand Down Expand Up @@ -283,6 +288,7 @@ class GenerateAssertionsPage extends React.Component<GenerateAssertionsPageProps
<thead>
<tr>
<th>Contest</th>
<th>County</th>
<th>Assertion Generation Status</th>
<th>Advise Retry</th>
<th>Winner</th>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Expand Down
9 changes: 8 additions & 1 deletion client/src/types/dos.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ declare namespace DOS {
standardizingContests?: boolean;
generatingAssertions?: boolean;
assertionsGenerated?: boolean;
generateAssertionsSummaries: DOS.GenerateAssertionsSummary[];
generateAssertionsSummaries: DOS.GenerateAssertionsSummaryWithCounty[];
assertionGenerationStatuses?: DOS.AssertionGenerationStatuses;
type: 'DOS';
}
Expand Down Expand Up @@ -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;
Expand All @@ -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[];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;

/**
Expand Down Expand Up @@ -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<GenerateAssertionsSummary> my_generate_assertions_summaries;
private final List<GenerateAssertionsSummaryWithCounty> 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.
Expand All @@ -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,
Expand All @@ -145,7 +139,7 @@ protected DoSDashboardRefreshResponse(final ASMState the_asm_state,
final AuditInfo the_audit_info,
final SortedMap<Long, AuditReason> the_audit_reasons,
final SortedMap<Long, AuditType> the_audit_types,
final List<GenerateAssertionsSummary> the_generate_assertions_summaries) {
final List<GenerateAssertionsSummaryWithCounty> 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;
Expand Down Expand Up @@ -233,17 +227,49 @@ 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<GenerateAssertionsSummary> 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());
}


/**
* 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<GenerateAssertionsSummaryWithCounty> addCountiesToSummaries() {
List<GenerateAssertionsSummaryWithCounty> generateAssertionsSummaries = new ArrayList<>();

// Load all the Generate Assertions Summaries from the database into the generate_assertions_list.
final List<GenerateAssertionsSummary> 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<ContestResult> cr = ContestResultQueries.find(summary.getContestName());
String countyName = "";
if (cr.isEmpty() || cr.get().getCounties().isEmpty()) {
// 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<County> counties = cr.get().getCounties();
if (counties.size() == 1) {
countyName = counties.stream().findFirst().get().name();
} else {
// Must be >1 because we already checked for zero.
countyName = MULTIPLE_COUNTIES;
}
}
// Add the summary in, whether we found a county or not.
generateAssertionsSummaries.add(new GenerateAssertionsSummaryWithCounty(summary, countyName));
}
return generateAssertionsSummaries;
}

/**
Expand All @@ -267,4 +293,12 @@ private static SortedMap<Long, CountyDashboardRefreshResponse> 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
){};
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<ContestResult>, containing the contest requested by name if present,
* otherwise empty.
*/
public static Optional<ContestResult> find(final String contestName) {
final Session s = Persistence.currentSession();
final Query<ContestResult> 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.
Expand Down
11 changes: 11 additions & 0 deletions server/eclipse-project/src/test/workflows/demo1_defineAudit.http
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit ac60bd5

Please sign in to comment.