Skip to content

Commit

Permalink
Merge pull request #155 from DemocracyDevelopers/100-persist-irvballo…
Browse files Browse the repository at this point in the history
…tinterpretation

100 persist irvballotinterpretation
  • Loading branch information
vteague authored Jul 23, 2024
2 parents 6b9c1e7 + ac7802e commit 082738a
Show file tree
Hide file tree
Showing 34 changed files with 766 additions and 2,707 deletions.
3 changes: 2 additions & 1 deletion client/src/component/AuditReportForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ const REPORT_TYPES: ReportType[] = [
{key:'ResultReport', label:'Result Report'},
{key:'ActivityReport', label:'Activity Report'},
{key:'StateReport', label:'State Report'},
{key:'JSON', label:'Json Reports'}
{key:'JSON', label:'Json Reports'},
{key:'ranked_ballot_interpretation', label:'Ranked vote interpretation'}
];


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,6 @@ public class GetAssertionsRequest extends ContestRequest {
private static final org.apache.log4j.Logger LOGGER =
LogManager.getLogger(GetAssertionsRequest.class);

/**
* The winner, as stated by the request. This is written into response metadata
* _without_ being checked.
*/
public final String winner;

/**
* The risk limit for the audit, expected to be in the range [0,1]. Defaults to zero, because
* then we know we will never mistakenly claim the risk limit has been met.
Expand All @@ -64,19 +58,17 @@ public class GetAssertionsRequest extends ContestRequest {
* @param contestName the name of the contest
* @param totalAuditableBallots the total number of ballots in the universe.
* @param candidates a list of candidates by name
* @param winner the winner's name
* @param riskLimit the risk limit for the audit, expected to be in the range [0,1].
*/
@ConstructorProperties({"contestName", "totalAuditableBallots", "candidates", "winner", "riskLimit"})
public GetAssertionsRequest(String contestName, int totalAuditableBallots, List<String> candidates,
String winner, BigDecimal riskLimit) {
BigDecimal riskLimit) {
super(contestName, totalAuditableBallots, candidates);

final String prefix = "[GetAssertionsRequest constructor]";
LOGGER.debug(String.format("%s Making GetAssertionsRequest for contest %s", prefix,
contestName));

this.winner = winner;
this.riskLimit = riskLimit;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
package au.org.democracydevelopers.corla.endpoint;

import au.org.democracydevelopers.corla.model.ContestType;
import au.org.democracydevelopers.corla.model.GenerateAssertionsSummary;
import au.org.democracydevelopers.corla.query.GenerateAssertionsSummaryQueries;
import com.google.gson.Gson;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
Expand All @@ -31,7 +33,11 @@
import us.freeandfair.corla.controller.ContestCounter;
import us.freeandfair.corla.endpoint.AbstractDoSDashboardEndpoint;
import us.freeandfair.corla.model.*;
import us.freeandfair.corla.persistence.Persistence;

import java.util.List;
import java.util.Optional;
import java.util.Set;

/**
* An abstract endpoint for communicating with raire. Includes all the information for collecting IRV contests
Expand Down Expand Up @@ -92,7 +98,7 @@ protected void reset() {
* @return A list of all ContestResults for IRV contests.
* @throws RuntimeException if it encounters contests with a mix of IRV and any other contest type.
*/
public static List<ContestResult> getIRVContestResults() {
protected static List<ContestResult> getIRVContestResults() {
final String prefix = "[getIRVContestResults]";
final String msg = "Inconsistent contest types:";

Expand All @@ -101,11 +107,11 @@ public static List<ContestResult> getIRVContestResults() {
.filter(cr -> cr.getContests().stream().map(Contest::description)
.anyMatch(d -> d.equalsIgnoreCase(ContestType.IRV.toString()))).toList();

// The above should be sufficient, but just in case, check that each contest we found _all_ matches IRV, and
// throw a RuntimeException if not.
// The above should be sufficient, but just in case, check that each contest we found _all_
// matches IRV, and throw a RuntimeException if not.
for (final ContestResult cr : results) {
if (cr.getContests().stream().map(Contest::description)
.anyMatch(d -> !d.equalsIgnoreCase(ContestType.IRV.toString()))) {
.anyMatch(d -> !d.equalsIgnoreCase(ContestType.IRV.toString()))) {
LOGGER.error(String.format("%s %s %s", prefix, msg, cr.getContestName()));
throw new RuntimeException(msg + cr.getContestName());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,14 @@
import us.freeandfair.corla.Main;
import us.freeandfair.corla.model.Choice;
import us.freeandfair.corla.model.ContestResult;
import us.freeandfair.corla.persistence.Persistence;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.*;
import java.util.stream.Collectors;

/**
* The Generate Assertions endpoint. Takes a GenerateAssertionsRequest, and optional parameters
Expand Down Expand Up @@ -94,7 +94,7 @@ public class GenerateAssertions extends AbstractAllIrvEndpoint {
/**
* Default winner to be used in the case where winner is unknown.
*/
protected static final String UNKNOWN_WINNER = "Unknown";
public static final String UNKNOWN_WINNER = "Unknown";

/**
* {@inheritDoc}
Expand Down Expand Up @@ -133,7 +133,7 @@ public String endpointBody(final Request the_request, final Response the_respons
final String contestName = the_request.queryParamOrDefault(CONTEST_NAME, "");

// Get all the IRV contest results.
final List<ContestResult> IRVContestResults = AbstractAllIrvEndpoint.getIRVContestResults();
final List<ContestResult> IRVContestResults = getIRVContestResults();

try {
if(validateParameters(the_request)) {
Expand Down Expand Up @@ -166,6 +166,9 @@ public String endpointBody(final Request the_request, final Response the_respons
serverError(the_response, e.getMessage());
}

// The only change is updating the winners in the IRV ContestResults.
Persistence.flush();

return my_endpoint_result.get();
}

Expand Down Expand Up @@ -248,29 +251,24 @@ protected GenerateAssertionsResponseWithErrors generateAssertionsUpdateWinners(L
final boolean gotRaireError = raireResponse.containsHeader(RaireServiceErrors.ERROR_CODE_KEY);

if (statusCode == HttpStatus.SC_OK && !gotRaireError) {
// OK response. Update the stored winner and return it.
// OK response. Return the winner.

LOGGER.debug(String.format("%s %s %s.", prefix, "OK response received from RAIRE for",
contestName));
GenerateAssertionsResponse responseFromRaire = Main.GSON.fromJson(EntityUtils.toString(raireResponse.getEntity()),
GenerateAssertionsResponse.class);

updateWinnersAndLosers(cr, candidates, responseFromRaire.winner);

LOGGER.debug(String.format("%s %s %s.", prefix,
"Completed assertion generation for contest", contestName));
return new GenerateAssertionsResponseWithErrors(contestName, responseFromRaire.winner, "");

} else if (statusCode == HttpStatus.SC_INTERNAL_SERVER_ERROR && gotRaireError) {
// Error response about a specific contest, e.g. "TIED_WINNERS".
// Return the error, record it.
// Error response about a specific contest, e.g. "TIED_WINNERS". Return the error.

final String code = raireResponse.getFirstHeader(RaireServiceErrors.ERROR_CODE_KEY).getValue();
LOGGER.debug(String.format("%s %s %s.", prefix, "Error response " + code,
"received from RAIRE for " + contestName));

updateWinnersAndLosers(cr, candidates, UNKNOWN_WINNER);

LOGGER.debug(String.format("%s %s %s.", prefix,
"Error response for assertion generation for contest ", contestName));
return new GenerateAssertionsResponseWithErrors(cr.getContestName(), UNKNOWN_WINNER, code);
Expand All @@ -282,7 +280,6 @@ protected GenerateAssertionsResponseWithErrors generateAssertionsUpdateWinners(L
LOGGER.error(String.format("%s %s", prefix, msg));
throw new RuntimeException(msg);
}

} catch (URISyntaxException | MalformedURLException e) {
// The raire service url is malformed, probably a config error.
final String msg = "Bad configuration of Raire service url: " + raireUrl + ". Check your config file.";
Expand Down Expand Up @@ -325,20 +322,6 @@ protected GenerateAssertionsResponseWithErrors generateAssertionsUpdateWinners(L
}
}

/**
* Update the contestresults in the database according to RAIRE's assessed winners. Set all
* non-winners to be losers, which means all candidates if the contest is un-auditable.
*
* @param cr the contest result, i.e. aggregaged (possibly cross-county) IRV contest.
* @param candidates the list of candidate names.
* @param winner the winner, as determined by raire.
* TODO This is currently non-functional - see Issue #136 <a href="https://github.com/DemocracyDevelopers/colorado-rla/issues/136">...</a>
*/
private void updateWinnersAndLosers(ContestResult cr, List<String> candidates, String winner) {
cr.setWinners(Set.of(winner));
cr.setLosers(candidates.stream().filter(c -> !c.equalsIgnoreCase(winner)).collect(Collectors.toSet()));
}

/**
* Validates the parameters of a request. For this endpoint, the query parameters are optional,
* but if the contest is present it should be non-null, and if a time limit is present it should
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@

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 @@ -50,6 +51,8 @@
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 @@ -165,14 +168,10 @@ public void getAssertions(final ZipOutputStream zos, final BigDecimal riskLimit,

// Iterate through all IRV Contests, sending a request to the raire-service for each one's assertions and
// collating the responses.
final List<ContestResult> IRVContestResults = AbstractAllIrvEndpoint.getIRVContestResults();
final List<ContestResult> IRVContestResults = getIRVContestResults();
for (final ContestResult cr : IRVContestResults) {

// Find the winner (there should only be one), candidates and contest name.
// TODO At the moment, the winner isn't yet set properly - will be set in the GenerateAssertions Endpoint.
// See https://github.com/DemocracyDevelopers/colorado-rla/issues/73
// For now, tolerate > 1; later, check.
final String winner = cr.getWinners().stream().findAny().orElse("UNKNOWN");
// Find the candidates and contest name.
final List<String> candidates = cr.getContests().stream().findAny().orElseThrow().choices().stream()
.map(Choice::name).toList();

Expand All @@ -185,7 +184,6 @@ public void getAssertions(final ZipOutputStream zos, final BigDecimal riskLimit,
cr.getContestName(),
cr.getBallotCount().intValue(),
candidates,
winner,
riskLimit
);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
Democracy Developers IRV extensions to colorado-rla.
@copyright 2024 Colorado Department of State
These IRV extensions are designed to connect to a running instance of the raire
service (https://github.com/DemocracyDevelopers/raire-service), in order to
generate assertions that can be audited using colorado-rla.
The colorado-rla IRV extensions are free software: you can redistribute it and/or modify it under the terms
of the GNU Affero General Public License as published by the Free Software Foundation, either
version 3 of the License, or (at your option) any later version.
The colorado-rla IRV extensions are distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License along with
raire-service. If not, see <https://www.gnu.org/licenses/>.
*/

package au.org.democracydevelopers.corla.model;

import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;

import javax.persistence.*;

import static java.util.Collections.min;


/**
* RAIRE (raire-java) generates a set of assertions for a given IRV contest, but it also returns
* the winner and (possibly) an informative error. These are stored in the database, which
* colorado-rla needs to read in order to produce IRV reports. This is read-only table here, with
* data identical to the corresponding class in raire-service.
*/
@Entity
@Table(name = "generate_assertions_summary")
public class GenerateAssertionsSummary {

/**
* Class-wide logger.
*/
private static final Logger LOGGER = LogManager.getLogger(GenerateAssertionsSummary.class);

/**
* ID.
*/
@Id
@Column(updatable = false, nullable = false)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;

/**
* Version. Used for optimistic locking.
*/
@Version
@Column(name = "version", updatable = false, nullable = false)
private long version;

/**
* Name of the contest.
*/
@Column(name = "contest_name", unique = true, updatable = false, nullable = false)
public String contestName;

/**
* Name of the winner of the contest, as determined by raire-java.
*/
@Column(name = "winner", updatable = false, nullable = false)
public String winner;

/**
* An error (matching one of the RaireServiceErrors.RaireErrorCodes), if there was one. Errors
* mean there are no assertions (nor winner), but some warnings
* (e.g. TIME_OUT_TRIMMING_ASSERTIONS) do have assertions and a winner, and allow the audit to
* continue.
*/
@Column(name = "error", updatable = false, nullable = false)
public String error;

/**
* A warning, if there was one, or emptystring if none. Warnings (e.g. TIME_OUT_TRIMMING_ASSERTIONS)
* mean that assertion generation succeeded and the audit can continue, but re-running with longer
* time allowed might be beneficial.
*/
@Column(name = "warning", updatable = false, nullable = false)
public String warning;

/**
* The message associated with the error, for example the names of the tied winners.
*/
@Column(name = "message", updatable = false, nullable = false)
public String message;

/**
* Default no-args constructor (required for persistence).
*/
public GenerateAssertionsSummary() {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,23 +22,20 @@
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.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;

import java.util.*;
import javax.persistence.*;

import us.freeandfair.corla.math.Audit;
import us.freeandfair.corla.model.*;

import java.math.BigDecimal;
import java.util.OptionalInt;

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 @@ -109,7 +106,8 @@ public IRVComparisonAudit() {
* thrown if an unexpected error arises when retrieving assertions from the database, or in the
* computation of optimistic and estimated sample sizes.
*
* @param contestResult The contest result (identifies the contest under audit).
* @param contestResult The contest result (identifies the contest under audit). This is updated
* with the winner returned
* @param riskLimit The risk limit.
* @param auditReason The audit reason.
*/
Expand Down
Loading

0 comments on commit 082738a

Please sign in to comment.