Skip to content

Commit 0a6ed5e

Browse files
committed
added mailroom 3 solution
1 parent 05cb383 commit 0a6ed5e

File tree

1 file changed

+253
-0
lines changed

1 file changed

+253
-0
lines changed
Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
#!/usr/bin/env python
2+
"""
3+
mailroom assignment
4+
5+
This version uses a dict for the main db, and a dict to"switch" on the user's
6+
input choices.
7+
8+
it also write the thank you letters to files.
9+
10+
"""
11+
12+
import sys
13+
import math
14+
from operator import itemgetter
15+
16+
# handy utility to make pretty printing easier
17+
from textwrap import dedent
18+
19+
20+
# In memory representation of the donor database
21+
# using a tuple for each donor
22+
# -- kind of like a record in a database table
23+
# using a dict with a lower case version of the donor's name as the key
24+
# This makes it easier to have a 'normalized' key.
25+
# you could get a bit fancier by having each "record" be a dict, with
26+
# "name" and "donations" as keys.
27+
28+
def get_donor_db():
29+
return {'william gates iii': ("William Gates III", [653772.32, 12.17]),
30+
'jeff bezos': ("Jeff Bezos", [877.33]),
31+
'paul allen': ("Paul Allen", [663.23, 43.87, 1.32]),
32+
'mark zuckerberg': ("Mark Zuckerberg", [1663.23, 4300.87, 10432.0]),
33+
}
34+
35+
36+
def list_donors():
37+
"""
38+
Create a list of the donors as a string, so they can be printed
39+
40+
Not calling print from here makes it more flexible and easier to
41+
test
42+
"""
43+
listing = ["Donor list:"]
44+
for donor in donor_db.values():
45+
listing.append(donor[0])
46+
return "\n".join(listing)
47+
48+
49+
def print_donor_list():
50+
"""
51+
Doesn't do much, but keeps the printing separate
52+
"""
53+
print(list_donors())
54+
print()
55+
56+
57+
def find_donor(name):
58+
"""
59+
Find a donor in the donor db
60+
61+
:param: the name of the donor
62+
:returns: The donor data structure -- None if not in the donor_db
63+
"""
64+
key = name.strip().lower()
65+
return donor_db.get(key)
66+
67+
68+
def add_donor(name):
69+
"""
70+
Add a new donor to the donor db
71+
72+
:param: the name of the donor
73+
:returns: the new Donor data structure
74+
"""
75+
name = name.strip()
76+
donor = (name, [])
77+
donor_db[name.lower()] = donor
78+
return donor
79+
80+
81+
def gen_letter(donor):
82+
"""
83+
Generate a thank you letter for the donor
84+
85+
:param: donor tuple
86+
:returns: string with letter
87+
88+
note: This doesn't actually write to a file -- that's a separate
89+
function. This makes it more flexible and easier to test.
90+
"""
91+
return dedent('''Dear {0:s},
92+
93+
Thank you for your very kind donation of ${1:.2f}.
94+
It will be put to very good use.
95+
96+
Sincerely,
97+
-The Team
98+
'''.format(donor[0], donor[1][-1]))
99+
100+
101+
def take_donation():
102+
"""
103+
Ask user for donation amount, and then add it to the DB
104+
"""
105+
# Now prompt the user for a donation amount to apply. Since this is
106+
# also an exit point to the main menu, we want to make sure this is
107+
# done before mutating the db.
108+
print("in take_donation")
109+
name = input("Enter a donor name (new or existing): \n >")
110+
while True:
111+
amount_str = input("Enter a donation amount (or <enter> to exit)> ").strip()
112+
if not amount_str:
113+
# if they provide no input, go back to previous menu
114+
return
115+
# Make sure amount is a valid amount before leaving the input loop
116+
try:
117+
amount = float(amount_str)
118+
# extra check here -- unlikely that someone will type "NaN", but
119+
# it IS possible, and it is a valid floating point number:
120+
# http://en.wikipedia.org/wiki/NaN
121+
if math.isnan(amount) or math.isinf(amount) or round(amount, 2) == 0.00:
122+
raise ValueError
123+
except ValueError:
124+
print("error: donation amount is invalid\n")
125+
continue
126+
else:
127+
break
128+
129+
donor = find_donor(name)
130+
# If the donor is not found, it's a new donor
131+
if donor is None:
132+
# add the new donor to the database
133+
donor = add_donor(name)
134+
135+
# Record the donation
136+
donor[1].append(amount)
137+
# print the thank you letter
138+
print(gen_letter(donor))
139+
140+
141+
def sort_key(item):
142+
# used to sort on name in donor_db
143+
return item[1]
144+
145+
146+
def generate_donor_report():
147+
"""
148+
Generate the report of the donors and amounts donated.
149+
150+
:returns: the donor report as a string.
151+
"""
152+
# First, reduce the raw data into a summary list view
153+
report_rows = []
154+
for (name, gifts) in donor_db.values():
155+
total_gifts = sum(gifts)
156+
num_gifts = len(gifts)
157+
avg_gift = total_gifts / num_gifts
158+
report_rows.append((name, total_gifts, num_gifts, avg_gift))
159+
160+
# sort the report data
161+
report_rows.sort(key=itemgetter(1), reverse=True)
162+
report = []
163+
report.append("{:25s} | {:11s} | {:9s} | {:12s}".format("Donor Name",
164+
"Total Given",
165+
"Num Gifts",
166+
"Average Gift"))
167+
report.append("-" * 66)
168+
for row in report_rows:
169+
report.append("{:25s} ${:10.2f} {:9d} ${:11.2f}".format(*row))
170+
return "\n".join(report)
171+
172+
173+
def save_letters_to_disk():
174+
"""
175+
make a letter for each donor, and save it to disk.
176+
"""
177+
for donor in donor_db.values():
178+
letter = gen_letter(donor)
179+
# I don't like spaces in filenames...
180+
filename = donor[0].replace(" ", "_") + ".txt"
181+
print("writing letter to:", donor[0])
182+
open(filename, 'w').write(letter)
183+
184+
185+
def print_donor_report():
186+
print(generate_donor_report())
187+
188+
189+
def return_to_menu():
190+
''' Return True to trigger exit out of sub-loop'''
191+
return True
192+
193+
194+
def send_thank_you():
195+
"""
196+
Execute the logic to record a donation and generate a thank you message.
197+
"""
198+
# Read a valid donor to send a thank you from, handling special commands to
199+
# let the user navigate as defined.
200+
prompt = ("To send a thank you, select one:\n\n"
201+
"(1) Update donor and send thank-you\n"
202+
"(2) List all existing DONORS\n"
203+
"(3) Return to main menu\n > ")
204+
selection_dict = {"1": take_donation,
205+
"2": print_donor_list,
206+
"3": return_to_menu,
207+
}
208+
run_menu(prompt, selection_dict)
209+
210+
def main_menu():
211+
"""
212+
Run the main menu for mailroom
213+
"""
214+
prompt = dedent('''
215+
Choose an action:
216+
217+
(1) - Send a Thank You
218+
(2) - Create a Report
219+
(3) - Send letters to everyone
220+
(4) - Quit
221+
222+
> ''')
223+
224+
selection_dict = {"1": send_thank_you,
225+
"2": print_donor_report,
226+
"3": save_letters_to_disk,
227+
"4": quit}
228+
229+
run_menu(prompt, selection_dict)
230+
231+
232+
def run_menu(prompt, selection_dict):
233+
"""
234+
run an interactive menu
235+
236+
:param prompt: What you want to ask the user
237+
238+
:param selection_dict: Dict of possible user impots mapped to
239+
the actions to take.
240+
"""
241+
while True:
242+
selection = input(prompt).strip().lower()
243+
try:
244+
if selection_dict[selection]():
245+
# break out of the loop if action returns True
246+
break
247+
except KeyError:
248+
print("error: menu selection is invalid!")
249+
250+
251+
if __name__ == "__main__":
252+
donor_db = get_donor_db()
253+
main_menu()

0 commit comments

Comments
 (0)