Spaces:
Running
Running
from src.donorframe import DonorFrame | |
from pathlib import Path | |
import pandas as pd | |
import numpy as np | |
import re | |
from pandas.errors import ParserError | |
from shiny import App, render, reactive, ui | |
from shiny.types import SilentException, FileInfo | |
def contains_special_characters(x): | |
pattern = re.compile(r"[^a-zA-Z0-9\_\-\/]") | |
return isinstance(x, str) and pattern.search(x) | |
def ui_card(title, *args): | |
return ( | |
ui.div( | |
{ | |
"class": "card mb-4", | |
"style": "width: 300px; border-color: #1c66e2; border-width: 2px", | |
}, | |
ui.div(title, class_="card-header"), | |
ui.div({"class": "card-body"}, *args), | |
), | |
) | |
www_dir = Path(__file__).parent / "www" | |
page_dependencies = ui.tags.head( | |
ui.tags.link(rel="shortcut icon", href="www/favicon.ico"), | |
ui.tags.link( | |
href="https://fonts.googleapis.com/css2?family=Barlow&display=swap", | |
rel="stylesheet", | |
), | |
) | |
app_ui = ui.page_fluid( | |
page_dependencies, | |
ui.tags.title("Donor Formatter"), | |
# ui.tags.style( | |
# """ | |
# body { | |
# font-family: Helvetica, sans-serif; | |
# background-color: #f8f9fa; | |
# color: #084fc9; | |
# } | |
# input { | |
# background-color: #f0f0f0; /* light gray for input fields */ | |
# border: 1px solid #ccc; | |
# padding: 5px 10px; | |
# margin: 5px; | |
# } | |
# button { | |
# background-color: navy; | |
# color: white; | |
# border: none; | |
# padding: 10px 20px; | |
# text-align: center; | |
# text-decoration: none; | |
# font-size: 16px; | |
# margin: 4px 2px; | |
# transition-duration: 0.4s; | |
# cursor: pointer; | |
# } | |
# button:hover { | |
# background-color: #1c66e2; /* a darker shade of navy for hover effect */ | |
# color: white; | |
# } | |
# h2 { | |
# font-size: 36px; | |
# color: #084fc9; | |
# font-family: 'Barlow', sans-serif; | |
# } | |
# /* Additional styles can be added here */ | |
# """ | |
# ), | |
ui.h2("The Donor Formatter"), | |
ui.layout_sidebar( | |
ui.sidebar( | |
{"class": "data-panel"}, | |
ui.tags.a( | |
"State Links", | |
href="https://docs.google.com/spreadsheets/d/1IyVlvVcU2xiuYyZW31jSRFQ6MRrwBp1pRhz-QoUdEnI/edit?usp=sharing", | |
), | |
ui.tags.a( | |
"Format Index", | |
href="https://docs.google.com/spreadsheets/d/1JNt3W_VA_HUAlxuDfh2g_--9zIu9NGizeUdq76j7yjI/edit?usp=sharing", | |
), | |
ui.input_file("donor_file", "Choose files to upload:", multiple=True), | |
ui.input_selectize( | |
"source", | |
"Select source:", | |
{ | |
"National": {"FEC": "FEC"}, | |
"City": { | |
"ATL": "Atlanta", | |
"ATX": "Austin, TX", | |
"CLB": "Columbus, OH", | |
"DTX": "Dallas, TX", | |
"DEN": "Denver", | |
"DET": "Detroit", | |
"LA_C": "Los Angeles", | |
"MNSTP": "Minneapolis", | |
"MNSTP_OLD": "Minneapolis (Old)", | |
"NYC": "New York City", | |
"SLC": "Salt Lake City", | |
"SA": "San Antonio", | |
"SJ": "San Jose", | |
"DC": "Washington, D.C.", | |
"DC_NEW": "Washington, D.C. (*NEW*)", | |
"Wayne_Co": "Wayne County", | |
}, | |
"State": { | |
"AL": "Alabama", | |
"AK": "Alaska", | |
"AZ": "Arizona", | |
"AR": "Arkansas", | |
"CA": "California", | |
"CO": "Colorado", | |
"CT": "Connecticut", | |
"DE": "Delaware", | |
"FL": "Florida", | |
"GA": "Georgia (New Transaction Search)", | |
"GA_old": "Georgia (Old Finance Website)", | |
"HI": "Hawaii", | |
"ID": "Idaho", | |
"IL": "Illinois", | |
"IN": "Indiana", | |
"IA": "Iowa", | |
"KS": "Kansas", | |
"KY": "Kentucky", | |
"LA": "Louisiana", | |
"ME": "Maine", | |
"MA": "Massachusetts", | |
"MD": "Maryland", | |
"MI": "Michigan", | |
"MN": "Minnesota", | |
"MO": "Missouri", | |
"MT": "Montana", | |
"NE": "Nebraska", | |
"NH": "New Hampshire", | |
"NJ": "New Jersey", | |
"NM": "New Mexico", | |
"NY": "New York", | |
"NV": "Nevada", | |
"NC": "North Carolina", | |
"ND": "North Dakota", | |
"OH": "Ohio", | |
"OK": "Oklahoma", | |
"OR": "Oregon", | |
"PA": "Pennsylvania", | |
"RI": "Rhode Island", | |
"SC": "South Carolina", | |
"TN": "Tennessee", | |
"TX": "Texas", | |
"UT": "Utah", | |
"VA": "Virginia", | |
"VT": "Vermont", | |
"WA": "Washington", | |
"WV": "West Virginia", | |
"WI": "Wisconsin", | |
"WY": "Wyoming", | |
}, | |
}, | |
), | |
ui.panel_conditional("input.source === 'VA'", ui.input_text("recipient_name", "Filter Recipient (TN Only)", "")), | |
ui.panel_conditional("input.source === 'VA'", ui.input_text("va_report_link", "Contributions URL", "")), | |
ui_card( | |
"File Facts:", | |
ui.output_text("total_donors"), | |
ui.output_text("abba_check"), | |
), | |
ui_card( | |
"Download your files:", | |
ui.download_button("export", "Download CSV"), | |
), | |
open="always", | |
width=350, | |
), | |
ui.card( | |
ui.div( | |
{"class": "data-panel", "style": "color: #003591"}, | |
ui.output_data_frame("result"), | |
), | |
ui.include_css(www_dir / "style.css"), | |
), | |
), | |
) | |
def server(input, output, session): | |
def recip_filter(): | |
if input.recipient_name(): | |
return input.recipient_name() | |
else: | |
return None | |
def get_file_name(): | |
if input.source() == 'VA': | |
return "va_candidate_donors" | |
paths = [file["name"] for file in input.donor_file()] | |
tmp_path = paths[0].split(".")[0] | |
return tmp_path | |
def compile_donors(): | |
file: list[FileInfo] | None = input.donor_file() | |
url = input.va_report_link() | |
if file is None and not url: | |
return pd.DataFrame() | |
if file: | |
paths = [file["datapath"] for file in input.donor_file()] | |
files = [DonorFrame(path, input.source(), recip_filter()) for path in paths] | |
print(files[0].recipient_name) | |
try: | |
dataframes = [file.format_donors(export=False) for file in files] | |
except KeyError: | |
raise SilentException() | |
return pd.concat(dataframes, ignore_index=True) | |
elif url and not file: | |
try: | |
dataframe = DonorFrame(data_path=url, source='VA').format_donors(export=False) | |
except ValueError as e: | |
raise e | |
return dataframe | |
def result(): | |
try: | |
donors_data = compile_donors() | |
except ParserError: | |
return pd.DataFrame({"No Data": ["No Data"]}) | |
try: | |
tmp_path = get_file_name() | |
except Exception as e: | |
print(f"{e=}") | |
if donors_data.empty: | |
return pd.DataFrame({"No Data": ["No Data"]}) | |
async def export(): | |
yield donors_data.drop("recipient_name", axis=1, errors="ignore").to_csv( | |
index=False | |
) | |
display_df = donors_data.replace("", np.nan) | |
display_df.dropna(thresh=4, inplace=True) | |
display_df.dropna(how="all", axis=1, inplace=True) | |
display_df.replace(np.nan, "", inplace=True) | |
try: | |
display_df['donation_amount'] = display_df['donation_amount'].replace('', '0').astype(int) | |
display_df = display_df.sort_values("donation_amount", ascending=False) | |
except KeyError: | |
raise SilentException() | |
display_df.columns = [ | |
name.title().replace("_", " ") for name in display_df.columns | |
] | |
return display_df | |
def total_donors(): | |
try: | |
donors_data = compile_donors() | |
except ParserError: | |
return "Total Donors: 0" | |
if donors_data.empty: | |
return "Total Donors: 0" | |
try: | |
total_donors_count = len(donors_data) | |
except (KeyError, ParserError): | |
return "Total Donors: 0" | |
return f"Total Donors: {total_donors_count}" | |
def abba_check(): | |
try: | |
donors_data = compile_donors() | |
except ParserError: | |
return "Formatted for ABBA: False" | |
if donors_data.empty: | |
return "Formatted for ABBA: False" | |
abba = (len(donors_data.columns) == 16) and donors_data.applymap( | |
contains_special_characters | |
).any().any() | |
return f"Formatted for ABBA: {abba}" | |
src_dir = Path(__file__).parent / "src" | |
app = App(app_ui, server) # , static_assets=www_dir) | |