Skip to content

Commit

Permalink
Work in progress...
Browse files Browse the repository at this point in the history
  • Loading branch information
davepeck committed Dec 1, 2023
1 parent c3e21c7 commit a8a6f6b
Show file tree
Hide file tree
Showing 5 changed files with 139 additions and 8 deletions.
9 changes: 9 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"preview": "vite preview"
},
"dependencies": {
"clsx": "^2.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
Expand Down
2 changes: 1 addition & 1 deletion server/utils/format.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
def fmt_usd(cents: int) -> str:
"""Format a value in cents as USD."""
return f"${cents / 100:,.2f}"
return f"${cents / 100:,.0f}"
15 changes: 14 additions & 1 deletion server/web.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from litestar.params import Body

from server.data.contacts.abbu import ZipABBUManager
from server.data.contacts.google import GoogleContactExportManager
from server.data.manager import DataManager
from server.data.search import ContactContributionSearcher

Expand All @@ -31,14 +32,26 @@ async def search(
data: t.Annotated[UploadFile, Body(media_type=RequestEncodingType.MULTI_PART)]
) -> dict:
"""Search a collection of contacts and summarize them."""
is_zip = data.content_type == "application/zip"
is_csv = data.content_type == "text/csv"
if not (is_zip or is_csv):
return {
"ok": False,
"message": "Invalid file type.",
"code": "invalid_file_type",
}
content = await data.read()
# Write to a temporary file; then pass it to the ZipABBUManager.
# Be sure to clean up the temporary file when we're done.
with tempfile.NamedTemporaryFile() as temp:
temp.write(content)
temp.flush()
data_manager = DataManager.default()
contact_manager = ZipABBUManager(temp.name)
contact_manager = (
ZipABBUManager(temp.name)
if is_zip
else GoogleContactExportManager(temp.name)
)
searcher = ContactContributionSearcher(data_manager)
results = list(searcher.search_and_summarize_contacts(contact_manager))
return {
Expand Down
120 changes: 114 additions & 6 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import clsx from "clsx";
import React, { useCallback, useState } from "react";
import "./App.css";

Expand Down Expand Up @@ -48,7 +49,94 @@ interface ErrorSearchResponse {
}

const formatPercent = (percent: number): string =>
`${(percent * 100).toFixed(0)}%`;
`${(percent * 100).toFixed(1)}%`;

const formatUSD = (cents: number): string =>
new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(cents / 100);

const formatParty = (party: string): string => {
switch (party) {
case "DEM":
return "Democrat";
case "REP":
return "Republican";
case "IND":
return "Independent";
case "OTH":
return "Other";
case "UNK":
return "Unknown";
default:
return party;
}
};

const partyBreakdownColorClassName = (
parties: Record<string, PartySummary>
): string => {
const party = Object.entries(parties).sort(
([, a], [, b]) => b.total_cents - a.total_cents
)[0][0];
switch (party) {
case "DEM":
return "text-blue-800";
case "REP":
return "text-red-800";
case "IND":
return "text-yellow-800";
default:
return "text-gray-800";
}
};

/**
* Create a copy of the `parties` structure. Count all non-UNK parties and get
* the percentage total for each.
*
* Then, take the UNK party and distribute its total with the same percentage
* breakdown as the other parties.
*
* If there is only one party, and it is UNK, leave it be.
*/
const revisePartyBreakdown = (
parties: Record<string, PartySummary>
): Record<string, PartySummary> => {
const nonUnknownParties = Object.entries(parties).filter(
([party]) => party !== "UNK"
);
if (nonUnknownParties.length === 0) {
return parties;
}
if (nonUnknownParties.length === Object.entries(parties).length) {
return parties;
}
const totalUnknown = parties.UNK.total_cents;
const revisedParties: Record<string, PartySummary> = {};
for (const [party, summary] of nonUnknownParties) {
revisedParties[party] = {
...summary,
total_cents: summary.total_cents + totalUnknown * summary.percent,
total_fmt: formatUSD(
summary.total_cents + totalUnknown * summary.percent
),
};
}
// Fix the percentages
const revisedTotal = Object.values(revisedParties).reduce(
(total, summary) => total + summary.total_cents,
0
);
for (const [party, summary] of Object.entries(revisedParties)) {
revisedParties[party] = {
...summary,
percent: summary.total_cents / revisedTotal,
};
}
return revisedParties;
};

type SearchResponse = SuccessSearchResponse | ErrorSearchResponse;

Expand All @@ -67,19 +155,39 @@ const SearchResults: React.FC<{ results: SearchResult[] }> = ({ results }) => (
<div className="mt-8">
<h2 className="font-bold text-2xl pb-4">Results</h2>
<ul>
{results.map((result) => (
<li key={result.contact.npa_id} className="pb-4 border-b-2">
<p className="font-bold text-3xl text-blue-900">
{results.map((result, i) => (
<li key={i} className="pb-4 border-b-2">
<p
className={clsx(
"font-bold text-3xl",
partyBreakdownColorClassName(
revisePartyBreakdown(result.summary.parties)
)
)}
>
{toTitleCase(
`${result.contact.first_name} ${result.contact.last_name}`
)}
</p>
<p>
{result.contact.city}, {result.contact.state}
{toTitleCase(result.contact.city)}, {result.contact.state}
</p>
{/* <p>{result.contact.phone}</p> */}
<p>Total: {result.summary.total_fmt}</p>
{/* Produce a party breakdown */}
<ul>
{Object.entries(revisePartyBreakdown(result.summary.parties)).map(
([party, partySummary]) => (
<li key={party}>
<p className="font-bold text-lg">
{formatParty(party)}: {partySummary.total_fmt} (
{formatPercent(partySummary.percent)})
</p>
</li>
)
)}
</ul>
{/* <ul>
{Object.entries(result.summary.committees).map(
([committeeId, committee]) => (
<li key={committeeId}>
Expand All @@ -90,7 +198,7 @@ const SearchResults: React.FC<{ results: SearchResult[] }> = ({ results }) => (
</li>
)
)}
</ul>
</ul> */}
</li>
))}
</ul>
Expand Down

0 comments on commit a8a6f6b

Please sign in to comment.