Skip to content

Commit

Permalink
Merge branch 'refs/heads/136-persist-winners-for-irv-contests' into 1…
Browse files Browse the repository at this point in the history
…00-persist-irvballotinterpretation

# Conflicts:
#	server/eclipse-project/src/main/resources/us/freeandfair/corla/persistence/entity_classes
  • Loading branch information
vteague committed Jul 20, 2024
2 parents 53020f4 + 908e92b commit 04c1834
Show file tree
Hide file tree
Showing 26 changed files with 377 additions and 2,670 deletions.
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 04c1834

Please sign in to comment.