Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

137 update generic reports with irv winners #185

Merged
merged 21 commits into from
Aug 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
84c48c3
Add sql to make a report that outputs the IRV summaries.
vteague Aug 7, 2024
ec72c50
tabulate.sql updated to include only PLURALITY contests.
vteague Aug 8, 2024
8bae62d
Updated contest_comparison.sql to include raw choices for uploaded ba…
vteague Aug 8, 2024
2e6f919
Updated contest_comparison.sql to include raw choices for audited bal…
vteague Aug 8, 2024
fe1eb34
Updated contest_comparison.sql to include raw choices for audited bal…
vteague Aug 8, 2024
e7e47e2
Some notes on what to change for IRV.
vteague Aug 9, 2024
e253096
Remove plurality tallying details (winner, margin, diluted margin) fr…
vteague Aug 10, 2024
f792c9a
Added button to export assertions as CSV and json.
vteague Aug 10, 2024
fe2e590
Add format name to zip filename.
vteague Aug 10, 2024
3d59c14
Refactored summary printing to be the same in StateReport and CountyR…
vteague Aug 10, 2024
ebf983b
Added button to export assertions as CSV and json when round is compl…
vteague Aug 11, 2024
6529783
Merge branch 'refs/heads/cherrypick-charlies-json-fix' into 137-updat…
vteague Aug 11, 2024
e339ff2
Added 'Download All' button.
vteague Aug 11, 2024
67e58ce
Fix min_margin for IRV in reports.
vteague Aug 13, 2024
0c9dcd0
Deriving IRV data from IRVComparisonAudits.
vteague Aug 15, 2024
7102f98
Store updates to ContestResults.
vteague Aug 15, 2024
d7dbc60
Revert addition of raw votes to contest_comparison.sql.
vteague Aug 15, 2024
3012281
Final commenting and logging for IRV reports.
vteague Aug 16, 2024
83410ad
A few more 'finals' and log messages.
vteague Aug 16, 2024
3069a76
Better commenting about the refactored row-construction function.
vteague Aug 16, 2024
d6dd3f3
Better commenting about the refactored row-construction function.
vteague Aug 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 33 additions & 28 deletions client/src/component/AuditReportForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const REPORT_TYPES: ReportType[] = [
{key:'ActivityReport', label:'Activity Report'},
{key:'StateReport', label:'State Report'},
{key:'JSON', label:'Json Reports'},
{key:'summarize_IRV', label:'IRV summaries'},
{key:'ranked_ballot_interpretation', label:'Ranked vote interpretation'}
];

Expand Down Expand Up @@ -75,38 +76,42 @@ class AuditReportForm extends React.Component<FormProps, FormState> {

return (
<Popover position={Position.BOTTOM_LEFT} canEscapeKeyClose={true}
enforceFocus={false} >
<Button large disabled={ !this.props.canRenderReport }
intent={ Intent.PRIMARY }>
Choose report to download</Button>
<div key="text" style={{margin: "10px", minWidth: "300px", maxWidth:"370px"}}>
enforceFocus={false}>

<Button large disabled={!this.props.canRenderReport} intent={Intent.PRIMARY}>
Choose report to download</Button>
<div key="text" style={{margin: "10px", minWidth: "300px", maxWidth: "370px"}}>
<h5>Audit reports</h5>
<FormGroup>

{REPORT_TYPES.map(ty => {
let key = ty.key;
let label = ty.label;
return <div className='checkbox'><Checkbox key={key}
label={label}
value={key}
checked={this.state.checkedReports[key] || false}
onChange={this.handleCheckboxChange}
style={{ minWidth: '10px', paddingLeft: '30px' }}/></div>
})
}
{REPORT_TYPES.map(ty => {
let key = ty.key;
let label = ty.label;
return <div className='checkbox'><Checkbox key={key}
label={label}
value={key}
checked={this.state.checkedReports[key] || false}
onChange={this.handleCheckboxChange}
style={{minWidth: '10px', paddingLeft: '30px'}}/>
</div>
})
}
</FormGroup>
<div style={{ display: "flex", justifyContent: "flex-end", marginTop: 15 }}>

<Button className={Classes.POPOVER_DISMISS}
intent={Intent.PRIMARY}
icon='import'
onClick={() => fetchAuditReport(Object.keys(this.state.checkedReports).join(","))}
>
Download Report
<div style={{display: "flex", flexDirection:"row", justifyContent: "space-between", marginTop: 15}}>
<Button className={Classes.POPOVER_DISMISS}
intent={Intent.PRIMARY}
icon='import'
onClick={() => fetchAuditReport(REPORT_TYPES.map(rt => rt.key).join(","))}>
Download All
</Button>
<Button className={Classes.POPOVER_DISMISS}
intent={Intent.PRIMARY}
icon='import'
onClick={() => fetchAuditReport(Object.keys(this.state.checkedReports).join(","))}>
Download Selected
</Button>
</div>
</div>
</Popover>
</div>
</div>
</Popover>

);
}
Expand Down
25 changes: 19 additions & 6 deletions client/src/component/DOS/Dashboard/Round/Control.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import * as React from 'react';
import { Button, Card, Elevation, Intent, ProgressBar } from '@blueprintjs/core';
import startNextRound from 'corla/action/dos/startNextRound';
import AuditReportForm from 'corla/component/AuditReportForm';
import exportAssertionsAsJson from "corla/action/dos/exportAssertionsAsJson";
import exportAssertionsAsCsv from "corla/action/dos/exportAssertionsAsCsv";

interface ControlProps {
canRenderReport: boolean;
Expand Down Expand Up @@ -39,17 +41,28 @@ class Control extends React.Component<ControlProps, ControlState> {
<div>
<h4>Round {currentRound} completed</h4>
<Button intent={Intent.PRIMARY}
onClick={waitForNextRound}>
onClick={waitForNextRound}>
Start round {currentRound + 1}
</Button>
</div>
<div>
<Button onClick={exportAssertionsAsJson} className='pt-button pt-intent-primary'>
Export Assertions as JSON
</Button>
</div>
<div>
<Button onClick={exportAssertionsAsCsv} className='pt-button pt-intent-primary'>
Export Assertions as CSV
</Button>
</div>
<div>
{canRenderReport && (<AuditReportForm
canRenderReport={canRenderReport}
/>
)}
</div> </div>
<div>
{canRenderReport && (<AuditReportForm
canRenderReport={canRenderReport}
/>
)}
</div>
</div>
</div>
);
};
Expand Down
13 changes: 13 additions & 0 deletions client/src/component/DOS/Dashboard/Round/Status.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import * as React from 'react';
import AuditReportForm from 'corla/component/AuditReportForm';
import {Button, Intent} from "@blueprintjs/core";
import exportAssertionsAsJson from "corla/action/dos/exportAssertionsAsJson";
import exportAssertionsAsCsv from "corla/action/dos/exportAssertionsAsCsv";

interface StatusProps {
auditIsComplete: boolean;
Expand Down Expand Up @@ -28,6 +31,16 @@ const Status = (props: StatusProps) => {
have finished this round.
</span>
</div>
<div>
<Button onClick={exportAssertionsAsJson} className='pt-button pt-intent-primary'>
Export Assertions as JSON
</Button>
</div>
<div>
<Button onClick={exportAssertionsAsCsv} className='pt-button pt-intent-primary'>
Export Assertions as CSV
</Button>
</div>
<div>
{canRenderReport && (<AuditReportForm
canRenderReport={canRenderReport}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@

import au.org.democracydevelopers.corla.communication.requestToRaire.GetAssertionsRequest;
import au.org.democracydevelopers.corla.communication.responseFromRaire.RaireServiceErrors;
import au.org.democracydevelopers.corla.query.GenerateAssertionsSummaryQueries;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
Expand All @@ -51,8 +50,6 @@
import us.freeandfair.corla.model.DoSDashboard;
import us.freeandfair.corla.util.SparkHelper;

import static au.org.democracydevelopers.corla.endpoint.GenerateAssertions.UNKNOWN_WINNER;

/**
* The Get Assertions endpoint. Takes a GetAssertionsRequest, and an optional format parameter specifying CSV or JSON,
* defaulting to json. Returns a zip of all assertions for all IRV contests, in the requested format.
Expand Down Expand Up @@ -132,11 +129,11 @@ public String endpointBody(final Request the_request, final Response the_respons
try {

final ZipOutputStream os = new ZipOutputStream(SparkHelper.getRaw(the_response).getOutputStream());
getAssertions(os, riskLimit, raireUrl, suffix);

the_response.header("Content-Type", "application/zip");
the_response.header("Content-Disposition", "attachment; filename*=UTF-8''assertions.zip");

the_response.header("Content-Disposition",
"attachment; filename*=UTF-8''assertions_" + suffix + ".zip");
getAssertions(os, riskLimit, raireUrl, suffix);
ok(the_response);

} catch (URISyntaxException | MalformedURLException e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
package au.org.democracydevelopers.corla.model;

import au.org.democracydevelopers.corla.query.AssertionQueries;
import au.org.democracydevelopers.corla.query.GenerateAssertionsSummaryQueries;
import com.google.inject.internal.util.ImmutableList;

import java.util.*;
Expand All @@ -35,7 +34,6 @@

import au.org.democracydevelopers.corla.model.assertion.Assertion;

import static au.org.democracydevelopers.corla.endpoint.GenerateAssertions.UNKNOWN_WINNER;
import static java.util.Collections.max;

/**
Expand Down Expand Up @@ -591,6 +589,14 @@ public ImmutableList<Assertion> getAssertions(){
return ImmutableList.<Assertion>builder().addAll(assertions).build();
}

/**
* Return the overall margin, which is the minimum margin of all the assertions.
* @return the minimum assertion margin.
*/
public int getMinMargin() {
return Collections.min(assertions.stream().map(Assertion::getMargin).toList());
}

/**
* This method checks whether this IRVComparisonAudit's list of assertions has been
* appropriately initialised, and throws a RunTimeException if not. It takes a string identifying
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,11 @@ public BigDecimal getDilutedMargin(){
return dilutedMargin;
}

/**
* Get the assertion's absolute margin.
*/
public int getMargin() { return margin; }

/**
* Get an (unmodifiable) map of the assertion's CVR ID-discrepancy records. This is used
* for testing purposes.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ public class IRVBallotInterpretation implements PersistentEntity {
@Version
private Long version;


/**
* The contest to which the interpreted vote belongs.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,19 @@
import java.io.InputStreamReader;
import java.io.BufferedReader;
import java.io.IOException;
import java.math.BigDecimal;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;

import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.util.HashMap;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import au.org.democracydevelopers.corla.endpoint.AbstractAllIrvEndpoint;
import au.org.democracydevelopers.corla.model.ContestType;
import au.org.democracydevelopers.corla.model.GenerateAssertionsSummary;
import au.org.democracydevelopers.corla.model.IRVComparisonAudit;
import au.org.democracydevelopers.corla.query.GenerateAssertionsSummaryQueries;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;

Expand All @@ -29,6 +33,9 @@

import org.hibernate.query.Query;

import us.freeandfair.corla.model.ComparisonAudit;
import us.freeandfair.corla.model.Contest;
import us.freeandfair.corla.model.ContestResult;
import us.freeandfair.corla.persistence.Persistence;

/** export queries **/
Expand Down Expand Up @@ -120,6 +127,10 @@ public static void customOut(final String query, final OutputStream os) {
**/
public static void jsonOut(final String query, final OutputStream os) {
final Session s = Persistence.currentSession();

// Make sure the contest_result table has the right info for IRV.
updateIRVContestResults(s);

final String withoutSemi = query.replace(";", "");
final String jsonQuery =
String.format("SELECT cast(row_to_json(r) as text)" + " FROM (%s) r", withoutSemi);
Expand Down Expand Up @@ -153,6 +164,10 @@ public static void jsonOut(final String query, final OutputStream os) {
/** send query results to output stream as csv **/
public static void csvOut(final String query, final OutputStream os) {
final Session s = Persistence.currentSession();

// Make sure the contest_result table has the right info for IRV.
updateIRVContestResults(s);

final String withoutSemi = query.replace(";", "");
s.doWork(new CSVWork(withoutSemi, os));
}
Expand All @@ -168,7 +183,7 @@ public static List<String> getSqlFolderFiles() {
final String[] fileNames = {"batch_count_comparison.sql", "contest.sql",
"contest_comparison.sql", "contest_selection.sql", "contests_by_county.sql",
"tabulate.sql", "tabulate_county.sql", "upload_status.sql", "seed.sql",
"ranked_ballot_interpretation.sql"};
"summarize_IRV.sql", "ranked_ballot_interpretation.sql"};
for (final String f : fileNames) {
paths.add(String.format("%s/%s", folder, f));
}
Expand Down Expand Up @@ -269,4 +284,63 @@ public static long custCopyOut(final String sql, OutputStream to, CopyManager cm
}
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the first sentence of the comment, clarify that the ContestResult structure does not have the correct values for those quantities for IRV.

/**
* This function deals, somewhat inelegantly, with the problem that the ContestResult data structure
* used in most queries does not have correct values for things like winners, losers, margin, and
* diluted margin for IRV contests. This function sets them manually from the
* GenerateAssertionsSummary table, then flushes the database so that the csv reports, which are
* based on database queries, get the right values from the contest_result table.
* @param s The current Hibernate session.
*/
private static void updateIRVContestResults(final Session s) {
final String prefix = "[updateIRVContestResults]";
LOGGER.debug(String.format("%s %s.", prefix,
"Updating IRV contest results from generate assertions summary"));

final List<ComparisonAudit> comparisonAudits = ComparisonAuditQueries.sortedList();

for(final ComparisonAudit ca : comparisonAudits) {
if(ca instanceof IRVComparisonAudit) {
final Set<String> choices = new HashSet<>();
// Get the choices for the contest. These should be the same for all the contests, but
// gather the whole set from all of them just in case.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to iterate over final Contest contest? It looks like line 309 is updating the choices set defined on line 304, yet choices has been defined final. (I think the rules are a bit weird/counter intuitive when objects are made final).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it's updating the object, but it's not reassigning the value, so 'final' is OK.

for(final Contest contest : ca.contestResult().getContests()) {
if (contest.description().equals(ContestType.IRV.toString())) {
contest.choices().stream().map(ch -> choices.add(ch.name()));
} else {
// We have an IRVComparisonAudit for a not-IRV contest. Definitely not supposed to happen.
final String msg = "IRV-type Comparison Audit encountered for non-IRV contest";
LOGGER.error(String.format("%s %s %s", prefix, msg, contest.name()));
throw new RuntimeException(msg+" "+contest.name());
}
}

final ContestResult contestResult = ca.contestResult();

// Use the choices and the summary to update the contest result in the database.
final Optional<GenerateAssertionsSummary> summary
= GenerateAssertionsSummaryQueries.matching(ca.getContestName());
if(summary.isPresent()) {
final String winner = summary.get().getWinner();
contestResult.setWinners(Set.of(winner));
choices.remove(winner);
contestResult.setLosers(choices);
contestResult.setMinMargin(((IRVComparisonAudit) ca).getMinMargin());
contestResult.setDilutedMargin(ca.getDilutedMargin());
} else {
// If no summary is present, just set the winner to be blank, the losers to be everyone,
// and the margins to be zero.
LOGGER.debug(String.format("%s %s %s", prefix, "Couldn't find summary for IRV contest",
ca.getContestName()));
contestResult.setWinners(Set.of());
contestResult.setLosers(choices);
contestResult.setMinMargin(0);
contestResult.setDilutedMargin(BigDecimal.ZERO);
}
}
}

s.flush();

}
}
Loading
Loading