Skip to content

Commit

Permalink
Implement Election w/ state model and check state on ElectionPage (#32)
Browse files Browse the repository at this point in the history
  • Loading branch information
pamtaro authored Oct 9, 2020
1 parent e286b1c commit f6ae393
Show file tree
Hide file tree
Showing 11 changed files with 83 additions and 32 deletions.
23 changes: 23 additions & 0 deletions src/components/ElectionPlaceholderMessage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from 'react';
import { Text } from '@fluentui/react';
import moment from 'moment';
import { useTheme } from '@fluentui/react-theme-provider';

// TODO #?? Resolve internalization of dates using i18n
const dateFormat = 'MM/DD/YYYY h:mm a';

export interface ElectionPlaceholderMessageProps {
endDate: string;
}

const ElectionPlaceholderMessage: React.FunctionComponent<ElectionPlaceholderMessageProps> = ({ endDate }) => {
const theme = useTheme();
return (
<Text variant="large" styles={{ root: { color: theme.palette.neutralSecondary } }}>
The election is not finished at this time. Please come back to check for results after
{' ' + moment(endDate).format(dateFormat)}.
</Text>
);
};

export default ElectionPlaceholderMessage;
9 changes: 7 additions & 2 deletions src/components/ElectionResults.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Story, Meta } from '@storybook/react/types-6-0';
import ElectionResults from './ElectionResults';

import electionDescription from '../mocks/description.json';
import { ElectionDescription } from '../models/election';
import { Election } from '../models/election';
import { queryArgTypes, QueryStoryArgs } from '../util/queryStory';

export default {
Expand All @@ -18,7 +18,12 @@ export default {
interface StoryArgs extends QueryStoryArgs {}

const Template: Story<StoryArgs> = ({ queryState }) => {
return <ElectionResults election={electionDescription as ElectionDescription} />;
var election = {
id: electionDescription.election_scope_id,
election_description: electionDescription,
state: 'Published',
} as Election;
return <ElectionResults election={election} />;
};

export const Success = Template.bind({});
Expand Down
12 changes: 6 additions & 6 deletions src/components/ElectionResults.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import { useElectionResults } from '../data/queries';
import { ElectionDescription } from '../models/election';
import { Election } from '../models/election';
import { ElectionResultsSummary } from '../models/tally';
import AsyncContent from './AsyncContent';
import ContestResults from './ContestResults';
Expand All @@ -9,14 +9,14 @@ import LargeCard from './LargeCard';
const errorMessage = 'Unable to retrieve election results at this time.';

export interface ElectionResultsProps {
election: ElectionDescription;
election: Election;
}

/**
* Render the results of the election
*/
const ElectionResults: React.FunctionComponent<ElectionResultsProps> = ({ election }) => {
const electionResultsQuery = useElectionResults(election.election_scope_id);
const electionResultsQuery = useElectionResults(election.id);

return (
<AsyncContent query={electionResultsQuery} errorMessage={errorMessage}>
Expand All @@ -29,7 +29,7 @@ const ElectionResults: React.FunctionComponent<ElectionResultsProps> = ({ electi
<ContestResults
results={contest.results}
contest={contest.description!}
candidates={election.candidates}
candidates={election.election_description.candidates}
/>
</LargeCard>
))}
Expand All @@ -40,12 +40,12 @@ const ElectionResults: React.FunctionComponent<ElectionResultsProps> = ({ electi
);
};

function getContests(election: ElectionDescription, results: ElectionResultsSummary) {
function getContests(election: Election, results: ElectionResultsSummary) {
const contests = Object.entries(results.election_results)
.map(([contestId, contestResults]) => ({
id: contestId,
results: contestResults,
description: election.contests.find((c) => c.object_id === contestId),
description: election.election_description.contests.find((c) => c.object_id === contestId),
}))
.filter((c) => !!c.description);
return contests;
Expand Down
4 changes: 2 additions & 2 deletions src/components/ElectionTitle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { useTheme } from '@fluentui/react-theme-provider';
// TODO #?? Resolve internalization of language using i18n
const defaultElectionName = 'Election';
// TODO #?? Resolve internalization of dates using i18n
const dateFormat = 'MM/DD/YYYY';
const dateFormat = 'MM/DD/YYYY h:mm a';

export interface ElectionTitleProps {
electionName?: string;
Expand All @@ -21,7 +21,7 @@ const ElectionTitle: React.FunctionComponent<ElectionTitleProps> = ({ electionNa
<Title title={electionName ?? defaultElectionName}>
<Text as="span" styles={{ root: { color: theme.palette.neutralSecondary } }}>{`${moment(startDate).format(
dateFormat
)}-${moment(endDate).format(dateFormat)}`}</Text>
)} - ${moment(endDate).format(dateFormat)}`}</Text>
</Title>
);
};
Expand Down
4 changes: 2 additions & 2 deletions src/data/DataAccess.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { ElectionDescription } from '../models/election';
import { Election } from '../models/election';
import { ElectionResultsSummary } from '../models/tally';
import { TrackedBallot } from '../models/tracking';

/**
* Provides access to election data and search functionality.
*/
export interface DataAccess {
getElections(): Promise<ElectionDescription[]>;
getElections(): Promise<Election[]>;
getElectionResults(electionId: string): Promise<ElectionResultsSummary>;
searchBallots(electionId: string, query: string): Promise<TrackedBallot[]>;
}
13 changes: 9 additions & 4 deletions src/data/PublishedDataAccess.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import path from 'path';
import { ElectionDescription } from '../models/election';
import { Election, ElectionDescription } from '../models/election';
import {
CiphertextAcceptedBallot,
PlaintextTally,
Expand All @@ -14,9 +14,14 @@ import { DataAccess } from './DataAccess';
* DataAccess implementation for static published ElectionGuard data.
*/
export class PublishedDataAccess implements DataAccess {
async getElections(): Promise<ElectionDescription[]> {
const election = await loadPublishedFile('description.json');
return [election as ElectionDescription];
async getElections(): Promise<Election[]> {
const electionDescription = await loadPublishedFile<ElectionDescription>('description.json');
const election: Election = {
id: electionDescription.election_scope_id,
election_description: electionDescription,
state: 'Published',
};
return [election];
}

async getElectionResults(electionId: string): Promise<ElectionResultsSummary> {
Expand Down
6 changes: 3 additions & 3 deletions src/data/queries.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useQuery } from 'react-query';
import { ElectionDescription } from '../models/election';
import { Election } from '../models/election';
import { ElectionResultsSummary } from '../models/tally';
import { TrackedBallot } from '../models/tracking';
import { useDataAccess } from './DataAccessProvider';
Expand All @@ -18,10 +18,10 @@ export interface QueryResult<T> {
}

/**
* Fetch the available election descriptions
* Fetch the available elections
* @param condition An optional boolean value which, if false, will prevent the query from running.
*/
export function useElections(condition: boolean = true): QueryResult<ElectionDescription[]> {
export function useElections(condition: boolean = true): QueryResult<Election[]> {
const dataAccess = useDataAccess();
return useQuery(QUERIES.ELECTIONS, () => dataAccess.getElections(), { enabled: condition });
}
Expand Down
11 changes: 8 additions & 3 deletions src/mocks/MockDataAccess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import mockDescription from './description.json';
import mockTally from './tally.json';
import mockBallots from './ballots.json';
import { CiphertextAcceptedBallot, transformBallotForTracking, transformTallyResults } from '../models/electionguard';
import { ElectionDescription } from '../models/election';
import { Election } from '../models/election';
import { ElectionResultsSummary } from '../models/tally';
import { TrackedBallot } from '../models/tracking';

Expand All @@ -17,8 +17,13 @@ const trackedBallots = (mockBallots as CiphertextAcceptedBallot[]).map((ballot)
* DataAccess implementation for in-memory synchronous mocked data
*/
export class MockDataAccess implements DataAccess {
async getElections(): Promise<ElectionDescription[]> {
return [mockDescription as any];
async getElections(): Promise<Election[]> {
const mockElection: Election = {
id: mockDescription.election_scope_id,
election_description: mockDescription,
state: 'Published',
};
return [mockElection];
}

async getElectionResults(electionId: string): Promise<ElectionResultsSummary> {
Expand Down
8 changes: 8 additions & 0 deletions src/models/election.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { InternationalizedText } from './internationalizedText';

export type ElectionState = 'New' | 'Open' | 'Closed' | 'Published';

export interface Election {
id: string;
election_description: ElectionDescription;
state: ElectionState;
}

export interface ElectionDescription {
election_scope_id: string;
start_date: string;
Expand Down
23 changes: 14 additions & 9 deletions src/pages/ElectionPage.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import React from 'react';
import { Stack } from '@fluentui/react';
import { Stack, Text } from '@fluentui/react';

import AsyncContent from '../components/AsyncContent';
import ElectionTitle from '../components/ElectionTitle';
import ElectionPlaceholderMessage from '../components/ElectionPlaceholderMessage';
import TrackerSearch from '../components/TrackerSearch';
import ElectionResults from '../components/ElectionResults';
import { useElections } from '../data/queries';
Expand All @@ -21,7 +22,7 @@ const ElectionPage: React.FunctionComponent<ElectionPageProps> = () => {
<Stack>
<AsyncContent query={electionsQuery} errorMessage="Unable to load the election at this time.">
{(elections) => {
const election = elections.find((e) => e.election_scope_id === electionId);
const election = elections.find((e) => e.id === electionId);

if (!election) {
return <Title title="We're having trouble finding this election. Please try again." />;
Expand All @@ -30,14 +31,18 @@ const ElectionPage: React.FunctionComponent<ElectionPageProps> = () => {
return (
<>
<ElectionTitle
electionName={translate(election!.name)}
startDate={election!.start_date}
endDate={election!.end_date}
electionName={translate(election!.election_description.name)}
startDate={election!.election_description.start_date}
endDate={election!.election_description.end_date}
/>

<TrackerSearch electionId={election!.election_scope_id} />

<ElectionResults election={election!} />
{election.state === 'Published' ? (
<>
<TrackerSearch electionId={election!.id} />
<ElectionResults election={election!} />
</>
) : (
<ElectionPlaceholderMessage endDate={election!.election_description.end_date} />
)}
</>
);
}}
Expand Down
2 changes: 1 addition & 1 deletion src/pages/HomePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const ElectionPage: React.FunctionComponent<HomePageProps> = () => {
<AsyncContent query={electionsQuery} errorMessage="Unable to load any elections at this time.">
{(elections) => {
const election = elections[0];
return <Redirect to={`/${election.election_scope_id}`} />;
return <Redirect to={`/${election.id}`} />;
}}
</AsyncContent>
</Stack>
Expand Down

0 comments on commit f6ae393

Please sign in to comment.