Skip to content

Commit

Permalink
Merge branch 'main' into 93-client-ui-for-generating-assertions
Browse files Browse the repository at this point in the history
  • Loading branch information
charliecarlton authored Aug 29, 2024
2 parents a2ad3d3 + 3bd231c commit 1e1b870
Show file tree
Hide file tree
Showing 29 changed files with 828 additions and 420 deletions.
4 changes: 3 additions & 1 deletion client/src/component/AuditReportForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ const REPORT_TYPES: ReportType[] = [
{key:'StateReport', label:'State Report'},
{key:'JSON', label:'Json Reports'},
{key:'summarize_IRV', label:'IRV summaries'},
{key:'ranked_ballot_interpretation', label:'Ranked vote interpretation'}
{key:'ranked_ballot_interpretation', label:'Ranked vote interpretation'},
{key:'assertions_json', label:'Assertions (json)'},
{key:'assertions_csv', label:'Assertions (csv)'}
];


Expand Down
13 changes: 0 additions & 13 deletions client/src/component/DOS/Dashboard/Round/Status.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
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 @@ -31,16 +28,6 @@ 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 @@ -22,22 +22,16 @@
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;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import us.freeandfair.corla.asm.ASMEvent;
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.net.http.HttpClient;
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 All @@ -54,7 +48,7 @@ public abstract class AbstractAllIrvEndpoint extends AbstractDoSDashboardEndpoin
/**
* GSON, for serialising requests.
*/
protected final Gson gson = new Gson();
protected final static Gson gson = new Gson();

/**
* Identify RAIRE service URL from config.
Expand All @@ -64,7 +58,7 @@ public abstract class AbstractAllIrvEndpoint extends AbstractDoSDashboardEndpoin
/**
* The httpClient used for making requests to the raire-service.
*/
protected final CloseableHttpClient httpClient = HttpClients.createDefault();
protected final static HttpClient httpClient = HttpClient.newHttpClient();


/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,28 +24,27 @@
import au.org.democracydevelopers.corla.communication.requestToRaire.GenerateAssertionsRequest;
import au.org.democracydevelopers.corla.communication.responseFromRaire.GenerateAssertionsResponse;
import com.google.gson.JsonSyntaxException;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.util.EntityUtils;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import spark.Request;
import spark.Response;
import us.freeandfair.corla.Main;
import us.freeandfair.corla.asm.ASMUtilities;
import us.freeandfair.corla.asm.DoSDashboardASM;
import us.freeandfair.corla.model.Choice;
import us.freeandfair.corla.model.ContestResult;
import us.freeandfair.corla.persistence.Persistence;
import us.freeandfair.corla.query.ComparisonAuditQueries;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.*;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.*;

import static us.freeandfair.corla.asm.ASMState.DoSDashboardState.PARTIAL_AUDIT_INFO_SET;

/**
* The Generate Assertions endpoint. Takes a GenerateAssertionsRequest, and optional parameters
* specifying a contest and time limit.
Expand Down Expand Up @@ -82,7 +81,7 @@ public class GenerateAssertions extends AbstractAllIrvEndpoint {
/**
* Query specifier for contest name.
*/
private static final String CONTEST_NAME = "contestName";
public static final String CONTEST_NAME = "contestName";

/**
* Default time limit for assertion generation.
Expand Down Expand Up @@ -118,6 +117,20 @@ public String endpointBody(final Request the_request, final Response the_respons
final String prefix = "[endpointBody]";
LOGGER.debug(String.format("%s %s.", prefix, "Received Generate Assertions request"));

// Check that the request is valid; abort if not.
if (!validateParameters(the_request)) {
final String msg = "Blank contest name or invalid time limit in Generate Assertions request";
LOGGER.debug(String.format("%s %s %s.", prefix, msg, the_request.body()));
badDataContents(the_response, msg);
}

// Check that the state is OK for assertion generation; abort if not.
if (!assertionGenerationAllowed(the_request)) {
final String msg = "Assertion generation not allowed in current state.";
LOGGER.error(String.format("%s %s.", prefix, msg));
illegalTransition(the_response, msg);
}

final List<GenerateAssertionsResponse> responseData;

final String raireUrl = Main.properties().getProperty(RAIRE_URL, "") + RAIRE_ENDPOINT;
Expand All @@ -133,29 +146,25 @@ public String endpointBody(final Request the_request, final Response the_respons
// Get all the IRV contest results.
final List<ContestResult> IRVContestResults = getIRVContestResults();

// Try to do the work.
try {
if(validateParameters(the_request)) {
if (contestName.isBlank()) {
// No contest was requested - generate for all.
if (contestName.isBlank()) {
// No contest was requested - generate for all.

responseData = generateAllAssertions(IRVContestResults, timeLimitSeconds, raireUrl);
} else {
// Generate for the specific contest requested.

responseData = generateAllAssertions(IRVContestResults, timeLimitSeconds, raireUrl);
} else {
// Generate for the specific contest requested.
responseData = List.of(generateAssertionsUpdateWinners(IRVContestResults, contestName,
timeLimitSeconds, raireUrl));
}

responseData = List.of(generateAssertionsUpdateWinners(IRVContestResults, contestName,
timeLimitSeconds, raireUrl));
}
the_response.header("Content-Type", "application/json");

the_response.header("Content-Type", "application/json");
okJSON(the_response, Main.GSON.toJson(responseData));

okJSON(the_response, Main.GSON.toJson(responseData));
LOGGER.debug(String.format("%s %s.", prefix, "Completed Generate Assertions request"));

LOGGER.debug(String.format("%s %s.", prefix, "Completed Generate Assertions request"));
} else {
final String msg = "Blank contest name or invalid time limit in Generate Assertions request";
LOGGER.debug(String.format("%s %s %s.", prefix, msg, the_request.body()));
badDataContents(the_response, msg);
}
} catch (IllegalArgumentException e) {
LOGGER.debug(String.format("%s %s.", prefix, "Bad Generate Assertions request"));
badDataContents(the_response, e.getMessage());
Expand All @@ -181,7 +190,7 @@ public String endpointBody(final Request the_request, final Response the_respons
* @param raireUrl the url where the raire-service is running.
*/
protected List<GenerateAssertionsResponse> generateAllAssertions(final List<ContestResult> IRVContestResults,
final double timeLimitSeconds, final String raireUrl) {
final double timeLimitSeconds, final String raireUrl) {
final String prefix = "[generateAllAssertions]";
LOGGER.debug(String.format("%s %s.", prefix, "Generating assertions for all IRV contests"));

Expand Down Expand Up @@ -234,27 +243,30 @@ protected GenerateAssertionsResponse generateAssertionsUpdateWinners(final List<
candidates
);

// Throws URISyntaxException or MalformedURLException if the raireUrl is invalid.
final HttpPost requestToRaire = new HttpPost(new URL(raireUrl).toURI());
requestToRaire.addHeader("content-type", "application/json");
requestToRaire.setEntity(new StringEntity(Main.GSON.toJson(generateAssertionsRequest)));

// Send it to the RAIRE service.
final HttpResponse raireResponse = httpClient.execute(requestToRaire);
// Throws URISyntaxException or MalformedURLException if the raireUrl is invalid.
final HttpResponse<String> raireResponse = httpClient.send(HttpRequest.newBuilder()
.uri(new URL(raireUrl).toURI())
.header("content-type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(Main.GSON.toJson(generateAssertionsRequest)))
.build(),
HttpResponse.BodyHandlers.ofString()
);
LOGGER.debug(String.format("%s %s %s.", prefix,
"Sent Assertion Request to Raire service for contest", contestName));

// Interpret the response.
final int statusCode = raireResponse.getStatusLine().getStatusCode();
final int statusCode = raireResponse.statusCode();

if (statusCode == HttpStatus.SC_OK) {
if (statusCode == HttpURLConnection.HTTP_OK && raireResponse.body() != null) {

// OK response, which may indicate either that assertion generation succeeded, or that it
// failed and raire generated a useful error. Return raire's response.
final GenerateAssertionsResponse responseFromRaire
= Main.GSON.fromJson(EntityUtils.toString(raireResponse.getEntity()), GenerateAssertionsResponse.class);
= gson.fromJson(raireResponse.body(), GenerateAssertionsResponse.class);

LOGGER.debug(String.format("%s %s %s %s.", prefix, responseFromRaire.succeeded ? "Success" : "Failure",
LOGGER.debug(String.format("%s %s %s %s.", prefix,
responseFromRaire.succeeded ? "Success" : "Failure",
"response for raire assertion generation for contest", contestName));
return responseFromRaire;

Expand Down Expand Up @@ -287,22 +299,21 @@ protected GenerateAssertionsResponse generateAssertionsUpdateWinners(final List<
final String msg = "Error generating request to Raire for contest ";
LOGGER.error(String.format("%s %s %s %s", prefix, msg, contestName, e.getMessage()));
throw new RuntimeException(msg + contestName + e.getMessage());
} catch (final ClientProtocolException e) {
// This also really shouldn't happen, but would happen if the effort to use the httpClient
// to send a message threw an exception.
final String msg = "Error sending request to Raire for contest ";
LOGGER.error(String.format("%s %s %s %s", prefix, msg, contestName, e.getMessage()));
throw new RuntimeException(msg + contestName + e.getMessage());
} catch (final NullPointerException e) {
// This also shouldn't happen - it would indicate an unexpected problem such as the httpClient
// returning a null response.
final String msg = "Error requesting or receiving assertions for contest ";
final String msg = "Error requesting or receiving assertions for contest";
LOGGER.error(String.format("%s %s %s.", prefix, msg, contestName));
throw new RuntimeException(msg + contestName);
} catch (final IOException e) {
// Generic error that can be thrown by the httpClient if the connection attempt fails.
final String msg = "I/O error during generate assertions attempt for contest ";
LOGGER.error(String.format("%s %s %s %s", prefix, msg, contestName, e.getMessage()));
final String msg = "I/O error during generate assertions attempt for contest";
LOGGER.error(String.format("%s %s %s. %s", prefix, msg, contestName, e.getMessage()));
throw new RuntimeException(msg + contestName + e.getMessage());
} catch (final InterruptedException e) {
// The http connection to RAIRE was interrupted.
final String msg = "Connection to RAIRE interrupted for contest";
LOGGER.error(String.format("%s %s %s. %s", prefix, msg, contestName, e.getMessage()));
throw new RuntimeException(msg + contestName + e.getMessage());
}
}
Expand Down Expand Up @@ -331,5 +342,38 @@ protected boolean validateParameters(final Request the_request) {
final String contestName = the_request.queryParams(CONTEST_NAME);
return contestName == null || !contestName.isEmpty();
}
}

/**
* Assertion generation is allowed to commence if we are in the DOS_INITIAL_STATE or the
* PARTIAL_AUDIT_INFO_SET state, otherwise it is not.
* This function also checks that there are no ComparisonAudits in the database, though this should
* always be true in the required states.
* @param the_request the endpoint request.
* @return true if we are in the right state and there are no ComparisonAudits in the database.
*/
private boolean assertionGenerationAllowed(final Request the_request) {
final String prefix = "[assertionGenerationAllowed]";
final String errorMsg = "Blocked assertion generation when requested for contest";

final DoSDashboardASM dashboardASM = ASMUtilities.asmFor(DoSDashboardASM.class, DoSDashboardASM.IDENTITY);

// Check that we're in either the initial state or the PARTIAL_AUDIT_INFO_SET state.
final boolean allowedState
= (dashboardASM.isInInitialState() || dashboardASM.currentState().equals(PARTIAL_AUDIT_INFO_SET));
if(!allowedState) {
LOGGER.debug(String.format("%s %s %s from illegal state %s.", prefix, errorMsg,
the_request.queryParams(CONTEST_NAME), dashboardASM.currentState()));
}

final boolean noComparisonAudits = ComparisonAuditQueries.count() == 0;

// Check that there are no ComparisonAudits in the database (which should not happen given the state).
if(!noComparisonAudits) {
LOGGER.debug(String.format("%s %s %s %s with %d ComparisonAudits in the database.", prefix, errorMsg,
the_request.queryParams(CONTEST_NAME), dashboardASM.currentState().toString(),
ComparisonAuditQueries.count()));
}

return allowedState && noComparisonAudits;
}
}
Loading

0 comments on commit 1e1b870

Please sign in to comment.