Skip to content

Commit

Permalink
Merge pull request #189 from DemocracyDevelopers/184-change-report-cl…
Browse files Browse the repository at this point in the history
…ient-ui-to-put-all-reports-into-the-same-drop-down

184 change report client UI to put all reports into the same drop down
  • Loading branch information
vteague authored Aug 29, 2024
2 parents f5bf31c + ec70191 commit 9fc6362
Show file tree
Hide file tree
Showing 18 changed files with 503 additions and 413 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 @@ -62,13 +62,7 @@ class GenerateAssertionsPage extends React.Component<GenerateAssertionsPageProps
support opportunistic discrepancy computation.
</p>
<p>
Only click on 'Generate Assertions' once! This prototype is
is not sophisticated enough to replace a contest's assertions
in the database (re-generating assertions will just replicate
those that already exist).
</p>
<p>
This prototype is also not sophisticated enough to give you
This prototype is not sophisticated enough to give you
feedback on how assertion generation is progressing ... but
once it is done either a green alert will tell you that it
was successful or a red one will tell you it has failed.
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,14 +190,14 @@ 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"));

// Iterate through all IRV Contests, sending a request to the raire-service for each one's assertions
final List<GenerateAssertionsResponse> responseData = IRVContestResults.stream().map(
r -> generateAssertionsUpdateWinners(IRVContestResults, r.getContestName(), timeLimitSeconds, raireUrl)
).toList();
).toList();

LOGGER.debug(String.format("%s %s.", prefix, "Completed assertion generation for all IRV contests"));
return responseData;
Expand All @@ -214,7 +223,7 @@ protected List<GenerateAssertionsResponse> generateAllAssertions(final List<Cont
* winner but may instead be UNKNOWN_WINNER and an error message.
*/
protected GenerateAssertionsResponse generateAssertionsUpdateWinners(final List<ContestResult> IRVContestResults,
final String contestName, final double timeLimitSeconds, final String raireUrl) {
final String contestName, final double timeLimitSeconds, final String raireUrl) {
final String prefix = "[generateAssertionsUpdateWinners]";
LOGGER.debug(String.format("%s %s %s.", prefix, "Generating assertions for contest ", contestName));

Expand All @@ -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 All @@ -311,6 +322,7 @@ protected GenerateAssertionsResponse generateAssertionsUpdateWinners(final List<
* 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
* be positive.
*
* @param the_request the request sent to the endpoint.
* @return true if the request's query parameters are valid.
*/
Expand All @@ -331,5 +343,39 @@ 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 9fc6362

Please sign in to comment.