class Individual(js.JsonSaveable): name = js.String() donations = js.List() def __init__(self, name, donations): self.name = name self.donations = donations def add_donation(self, donation): self.donations.append(donation) def number_donations(self): return int(len(self.donations)) def sum_donations(self): return sum(self.donations) def avg_donations(self): return self.sum_donations() / \ self.number_donations() def last_donation(self): return self.donations[-1] def json_format(self): setattr(self, 'donor_name', self.name) setattr(self, 'donor_donations', self.donations) return self.to_json_compat() @property def thank_you(self): """Add a donation to a donors records and print a report.""" return ( 'Thank you so much for the generous gift of ${0:.2f}, {1}!'.format( self.donations[-1], self.name))
class OtherSaveable(js.JsonSaveable): foo = js.String() bar = js.Int() def __init__(self, foo, bar): self.foo = foo self.bar = bar
class Donor(js.JsonSaveable): _name = js.String() _donations = js.List() def __init__(self, name, donations=None): self._name = name if donations is None: self._donations = [] else: self._donations = donations def __str__(self): return f"{self._name} donated ${sum(self._donations):,.2f}" def __repr__(self): return f"{self._name} donated ${sum(self._donations):,.2f}" @property def name(self): return self._name @property def donations(self): return self._donations @property def total(self): return sum(self._donations) @property def num_donation(self): return len(self._donations) @property def avg_donation(self): return sum(self._donations) / len(self._donations) def new_donation(self, donation): self._donations.append(donation) def __eq__(self, other): return sum(self._donations) == sum(other._donations) def __ne__(self, other): return not sum(self._donations) == sum(other._donations) def __lt__(self, other): return sum(self._donations) < sum(other._donations) def __gt__(self, other): return not sum(self._donations) < sum(other._donations) def __le__(self, other): return sum(self._donations) >= sum(other._donations) def __ge__(self, other): return not sum(self._donations) >= sum(other._donations)
class Donor(js.JsonSaveable): name = js.String() donations = js.List() def __init__(self, name, donations=None): if not name: raise ValueError("Donor name can not be empty") self.name = name if donations: self.donations = donations else: self.donations = [] @property def first_name(self): name_split = self.name.split() if len(name_split) >= 1: return name_split[0] @property def last_name(self): name_split = self.name.split() if len(name_split) == 1: return '' else: return ''.join(name_split[1:]) @property def donor_donations(self): """ Returns list of donor donations :return: list of donor donations """ return self.donations @property def donor_donations_sum(self): """ Returns sum of all donor donations :return: donor latest donation """ return sum(self.donations) @property def latest_donation(self): """ Returns donor latest donation :return: donor latest donation """ if self.donations: return self.donations[-1] def add_donation(self, amount): """ Adds donation to donor donations :return: """ if float(amount) <= 0: raise ValueError("donation amount can not be negative") self.donations.append(float(amount)) def generate_letter(self): """ Generate letter for donor """ return "Dear {},\n \nThank you for your generous donation {}.\n \n\n\t\tSincerely, \n\t\tLocal Charity". \ format(self.name, self.latest_donation)
class SingleDonor(js.JsonSaveable): """Provide a class for a single donor.""" _donations = js.List() _name = js.String() def __init__(self, _name, _donations): """Instantiate a SingleDonor class object.""" self._name = _name if isinstance(_donations, list): self._donations = _donations elif isinstance(_donations, tuple): self._donations = list(_donations) else: self._donations = [_donations] @property def name(self): """Provide a getter method for the name property.""" return self._name @property def donations(self): """Provide a getter method for the donations property.""" return self._donations def sort_by_total(self): """Provide a sort_key for sorting by total donations.""" return sum(self._donations) def sort_by_name(self): """Provide a sort_key for sorting by name.""" return self._name def __str__(self): """Return self._name.""" return self._name def __repr__(self): """Return SingleDonor("Name", [donations]).""" if len(self._donations) == 1: return 'SingleDonor("{}", {})'.format(self._name, self._donations[0]) else: return 'SingleDonor("{}", {})'.format(self._name, self._donations) def __eq__(self, other): """Return True if names and donations are the same.""" return (self._name, self._donations) == (other.name, other.donations) def __lt__(self, other): """Provide __lt__ method used in sorting somehow, I guess.""" return ((self._name, self._donations) < (other.name, other.donations)) def add_donation(self, amount): """Add a donation.""" self._donations.append(amount) def get_last_donation(self): """Return the last donation.""" return self._donations[-1] # def challenge(self, # factor, # min_donation, # max_donation, # projection): # """Return a SingleDonor class object or the projected contribution. # # Either multiply all donations by the factor, or # multiply only those donations which are above min_donation or # below max_donation, if any of these parameters is provided, # while the remaining donations remain unchanged. # If the projection is True, return the projected contribution. # """ # # Several safeguards # if type(factor) is str or factor <= 1: # raise ValueError("Factor must be a number > 1") # elif type(min_donation) is str or type(max_donation) is str: # raise ValueError("Input must be a number") # elif min_donation is not None and max_donation is not None: # raise ValueError("Min and max must not be both defined") # # # Helper function. # def subject_to_increase(x): # """Return True if x above min /below max or if min/max undefined.""" # if min_donation is not None: # return x > min_donation # elif max_donation is not None: # return x < max_donation # else: # return True # # # The only reason for the following ugly construct is because # # I couldn't imagine how to structure my solution to use map/filter # some_donations = list(filter(subject_to_increase, self.donations)) # updated_donations = list(map(lambda x: x * factor # if x in some_donations # else x, # self.donations # ) # ) # # # The following does the same as above but looks much clearer # # updated_donations = [x * factor # # if subject_to_increase(x) # # else x # # for x in self.donations # # ] # # # projected contribution = increased donationed minus old donations # if projection: # return sum(updated_donations) - sum(self.donations) # else: # return SingleDonor(self.name, updated_donations) def multiplier_factory(self, factor, min_donation, max_donation): """Create the multiplier function for use in challenge method. Args: factor (float): the multiplier to be locked in the return function min_donation (float or None): a condition to be locked in max_donation (None or float): a condition to be locked in Returns: a function which will multiply its argument by factor subject to conditions. """ # Several safeguards if type(factor) is str or factor <= 1: raise ValueError("Factor must be a number > 1") elif type(min_donation) is str or type(max_donation) is str: raise ValueError("Input must be a number") elif min_donation is not None and max_donation is not None: raise ValueError("Min and max must not be both defined") def func(x): def subject_to_increase(x): """Decide if the donation (i.e. x) must be increased.""" if min_donation is not None: return x > min_donation elif max_donation is not None: return x < max_donation else: return True if subject_to_increase(x): return factor * x else: return x return func def challenge(self, factor, min_donation, max_donation, projection): """Return an updated SingleDonor object or a projected contribution.""" multiplier = self.multiplier_factory(factor, min_donation, max_donation) updated_donations = list(map(multiplier, self.donations)) # projected contribution = increased donations minus old donations if projection: return sum(updated_donations) - sum(self.donations) else: return SingleDonor(self.name, updated_donations)
class Donor(js.JsonSaveable): """donor giving to organization""" id = js.Int() firstname = js.String() lastname = js.String() email = js.String() _donations = js.List() _donation_id = js.Int() def __init__(self, id, firstname=None, lastname=None, email=None): """args: id (int): identification for donor. Will try to force to int when initiated or raise error. firstname (str, optional): string representing given name lastnamt (str, optional): string representing surname _donations (list): contains Donation objects from donor _donation_id (int): tracks indentification for donations""" try: self.id = int(id) except ValueError: raise ValueError('id input should be interpreted as integer') self.firstname = firstname self.lastname = lastname self.email = email self._donations = [] self._donation_id = 1 def donation_total(self): """returns the total amount the donor has donated""" if self.donations: print(self.donations) return sum([i.amount for i in self.donations]) else: return 0 def add_donation(self, amount, date=datetime.datetime.utcnow()): """adds donation for user""" self._donations.append(Donation(amount=amount, date=date, id=self._donation_id)) self._donation_id += 1 def donation_count(self): """returns count of donations""" return len(self._donations) @property def donations(self): """returns donations""" return self._donations @property def fullname(self): """returns combine first and last name""" return " ".join([self.firstname, self.lastname]) def summarize_donor(self): """provides summary tuple of donor""" return (self.id, self.fullname, self.donation_total(), self.donation_count(), self.donation_total()/self.donation_count()) def __repr__(self): return str(self.to_json_compat())
class DonationController(js.JsonSaveable): """organization controller for donations""" name = js.String() donors = js.Dict() def __init__(self, name, donors=None): self.name = name if donors: self.donors = donors else: self.donors = {} def find_donor(self, donor: Donor): """searches through donor list and returns donor returns none if not found. Setup to first check for donor id if this fails, assumes integer and will check for that""" # TODO: refactor and add int method to donor to allow int(donor) # to return same as donor.id try: return self.donors.get(donor.id) except AttributeError: return self.donors.get(int(donor)) def create_donor(self, donor: Donor): """creates donor in donation controller. accepts donor object to register donor. args: donor: donor object to register with controller """ if self.find_donor(donor): raise KeyError('donor already exists') if isinstance(donor, Donor): try: self.donors[donor.id] = donor return donor.id except AttributeError: raise AttributeError('Donor object needs id') def build_donor_from_name(self, firstname, lastname): """interface to build donor from just name and automatically assign id""" self.create_donor( Donor(id=self.next_id, firstname=firstname, lastname=lastname)) def get_total_donations(self): """returns total donations in controller""" return sum([ k.donation_total() for i, k in self.donors.items() if k.donations ]) def create_donation(self, donor, amount, date=datetime.datetime.utcnow()): """creates donation in input donor""" if self.find_donor(donor) is None: raise IndexError('Donor does not exist') self.find_donor(donor).add_donation(amount) @property def next_id(self): """returns the next avaliable int id from list used to asign donor ids""" donor_id_list = {i for i in self.donors} if self.donors: max_id = max(donor_id_list) else: max_id = 1 # create range of ints non_used_ids = set(range(1, max_id + 1)) - donor_id_list if non_used_ids: return min(non_used_ids) else: return max_id + 1 def display_donors(self): """displays a list of donors in printed format""" donor_list = [ ":".join([str(donor_id), donor.fullname]) for donor_id, donor in self.donors.items() ] print("\n".join(donor_list)) def donor_report(self): """handles process for main screens report selection If the user (you) selected “Create a Report”, print a list of your donors, sorted by total historical donation amount. Include Donor Name, total donated, number of donations and average donation amount as values in each row. You do not need to print out all their donations, just the summary info. Using string formatting, format the output rows as nicely as possible. The end result should be tabular (values in each column should align with those above and below) After printing this report, return to the original prompt. At any point, the user should be able to quit their current task and return to the original prompt. From the original prompt, the user should be able to quit the script cleanly. Your report should look something like this: Donor Name | Total Given | Num Gifts | Average Gift ------------------------------------------------------------------ William Gates, III $ 653784.49 2 $ 326892.24 Mark Zuckerberg $ 16396.10 3 $ 5465.37 Jeff Bezos $ 877.33 1 $ 877.33 Paul Allen $ 708.42 3 $ 236.14 """ print(f"{'Donor Name':<26}|{'Total Given':^15}|" f"{'Num Gifts':^11}|{'Average Gift':^15}") print('-' * 70) donor_stats = [ donor.summarize_donor() for id, donor in self.donors.items() ] donor_stats.sort(key=lambda tup: tup[2], reverse=True) for summary in donor_stats: print(f"{summary[1]:<26} ${summary[2]/100:>13.2f} " f"{summary[3]:>10} ${summary[4]/100:>14.2f}") def send_letters_to_everyone( self, thank_you_directory=Path('/mailroom_thankyou_letters')): """creates thank yous for all donors and sends out""" # iterate through donors and donations to send thank yous for id, donor in self.donors.items(): file_name = "".join([str(id), '.txt']) full_path = thank_you_directory / file_name donor_info = donor.summarize_donor() thank_you_text = self.create_donation_thank_you( donor=donor, amount=donor_info[2]) try: print(full_path) with open(full_path, 'w') as f: f.write(thank_you_text) except FileNotFoundError: print( 'Mailroom thank you directory not found. Please create this directory first.' ) break else: print( f'Thank you letter for {id} created in "{thank_you_directory}"' ) def create_donation_thank_you(self, donor, amount): """prints thank you message to terminal for donation""" return f"""Dear {donor.fullname}, Thank you for your very kind donation of ${amount:.2f}. It will be put to very good use. Sincerely, -The Team""" def challenge(self, factor, min_donation=0, max_donation=1e9): """increases donations due to nice donor returns copy of donation controller with multiplied input""" if factor < 1: raise ValueError( 'Donors are not allowed to take $ from our cause. Please consider factor of 10 ;)' ) # copy generated to avoid messing with old database new_db = copy.deepcopy(self) mod_func = modifying_fun_generator(factor) mod_filter = filter_fun_generator(min_donation=min_donation, max_donation=max_donation) # seperate list made to simplify code as we need to regenerate dict donor_list = map(mod_func, filter(mod_filter, new_db.donors.values())) # rebuilds donors dict new_db.donors = {i.id: i for i in donor_list} return new_db def project_donation(self, factor, min_donation=0, max_donation=1e9): """provides projected donation amount assuming donor matches all donations""" return self.challenge(factor=factor, min_donation=min_donation, max_donation=max_donation).get_total_donations() def save(self, filename): """saves donation database""" with open(filename, 'w') as fp: json.dump(self.to_json_compat(), fp) return self.to_json_compat() @classmethod def load(cls, filename): """rebuilds database from file""" try: with open(filename, 'r') as fp: data = json.load(fp) return cls.from_json_dict(data) except FileNotFoundError: """creates new donation controller when file not found""" return cls(filename) def __eq__(self, other): return self.to_json_compat() == other.to_json_compat() def __repr__(self): return str(self.to_json_compat())
class NoInit(js.JsonSaveable): x = js.Int() y = js.String()
class Donor(js.JsonSaveable): _donation = js.List() _name = js.String() def __init__(self, name, donation=None): if name == '': self._name = 'Anonymous' else: self._name = name if donation is None: self._donation = [] elif isinstance(donation, (int, float)): self._donation = [donation] else: self._donation = donation @property def name(self): return self._name @name.setter def name(self, name): self._name = str(name) @property def donation(self): return self._donation @donation.setter def donation(self, donation): if type(donation) == list: self._donation = donation else: raise TypeError("Donation has to be a list of int") def add_donation(self, donation_amount): if donation_amount >= 0: self._donation.append(donation_amount) else: raise ValueError("Donation amount must be greater than 0.") def __repr__(self): return "Donor({} : {})".format(self._name, self._donation) def __str__(self): return "Donor: {} donated {}\n".format(self._name, self._donation) def donation_occurrences(self): return len(self._donation) def total_donation_amount(self): return sum(self._donation) def average_total_donor_amount(self): try: return self.total_donation_amount() / self.donation_occurrences() except ValueError: return self._donation def stats(self): return [ self.total_donation_amount(), self.donation_occurrences(), self.average_total_donor_amount() ]
class Donor(js.JsonSaveable): _name = js.String() _list_donations = js.List() _donation_count = js.Int() _amount = js.Float() def __init__(self, name, list_donations): self._name = name self._list_donations = list_donations self._donation_count = len(list_donations) self._amount = sum(list_donations) @property def name(self): return self._name @property def amount(self): return self._amount @amount.setter def amount(self, amount): self._amount = amount def add(self, donation_amount): self._amount += donation_amount self._donation_count += 1 self._list_donations.append(donation_amount) @property def donation_count(self): return self._donation_count @property def average(self): return self._amount / self._donation_count def get_letter_text(self, name, amount): msg = [] msg.append('Dear {},'.format(name)) msg.append( '\n\n\tThank you for your very kind donation of ${:.2f}.'.format( amount)) msg.append('\n\n\tIt will be put to very good use.') msg.append('\n\n\t\t\t\tSincerely,') msg.append('\n\t\t\t\t-The Team\n') return "".join(msg) def challenge(self, factor, min_donation=None, max_donation=None): """Get the sum of projection donation from a donor""" new_list = [] if min_donation is not None and max_donation is not None: #filter minimum & maximum new_list = list( filter(lambda donation: donation >= min_donation, self._list_donations)) new_list = list( filter(lambda donation_amount: donation_amount <= max_donation, new_list)) new_list = list(map(lambda x: x * factor, new_list)) elif min_donation is None and max_donation is not None: #filter maximum new_list = list( filter(lambda donation: donation <= max_donation, self._list_donations)) new_list = list(map(lambda x: x * factor, new_list)) elif min_donation is not None and max_donation is None: # filter minimum new_list = list( filter(lambda donation: donation >= min_donation, self._list_donations)) new_list = list(map(lambda x: x * factor, new_list)) else: # no minimum and maximum new_list = list(map(lambda x: x * factor, self._list_donations)) return sum(new_list) def __lt__(self, other): return self._amount < other._amount def __gt__(self, other): return self._amount > other._amount def __eq__(self, other): return self._amount == other._amount
class Donor(js.JsonSaveable): """ Class to store a donor record Args: full_name(str) format: <first name> [<middle_name>] <last_name> [,<suffix>] or first_name(str) middle_name(str) last_name(str) suffix(str) donation(float) amount of today's donation or donations(list) list of dicts containing donations Not typically specified ----------------------- did(str) string representation of a record UUID created(str) string representation of an utcnow isoformat timestamp with "Z" appended Usage: The class allows flexibility in how a record is set. It is allowable to create an empty instance of the class and update it piecemeal. It is also allowable to pass in all information needed to create a record using different strategies such as a full_name vs the constituent parts. In general, record manipulation is simply the name and donation. However, it is also allowable to set information that is normally auto-created such as the did, time created and entire donation list. This allows a record to be created from its repr which may have practical uses in record backup/recreation as needed. Example Use Cases: # empty donor record In [599]: d=Donor() In [600]: repr(d) Out[600]: "Donor(did='97d744de-de23-11e7-bee5-0800274b0a84', created='2017-12-11T03:30:11.077937Z' )" In [601]: # automatic parsing of full_name In [601]: d=Donor(full_name="sally q smith, iv") In [602]: repr(d) Out[602]: "Donor(did='067f51c4-de24-11e7-bee5-0800274b0a84', first_name='Sally', middle_name='Q', last_name='Smith', suffix='IV', created='2017-12-11T03:33:16.728654Z' )" In [603]: # name creation based name sub-components In [594]: d=Donor(first_name="joe", last_name="smith", donation=100) In [595]: repr(d) Out[595]: "Donor(did='7724e750-de23-11e7-bee5-0800274b0a84', first_name='Joe',last_name='Smith', donations=[{'amount': 100.0, 'date': '2017-12-11Z'}], created='2017-12-11T03:29:16.221954Z' )" In [596]: In [636]: d=Donor(full_name="john adams") In [637]: d.add_donation=1000 In [638]: repr(d) Out[638]: "Donor(did='7e93d724-de25-11e7-bee5-0800274b0a84', first_name='John',last_name='Adams', donations=[{'amount': 1000.0, 'date': '2017-12-11Z'}, {'amount': 1000.0, 'date': '2017-12-11Z'}], created='2017-12-11T03:43:47.686457Z' )" In [639]: """ created = js.String() audit = js.List() _did = js.String() _first_name = js.String() _last_name = js.String() _middle_name = js.String() _suffix = js.String() _donations = js.List() def __init__(self, did="", created="", full_name="", first_name="", last_name="", middle_name="", suffix="", donations=None, donation=None): self.__name__ = "Donor" # normally no did is passed in, so we create one if did == "": did = str(uuid.uuid1()) # create a uuid for each record self.did = did # normally, no timestamp is passed in, so we create one if created == "": created = datetime.datetime.utcnow().isoformat() + "Z" # keep track of record creation self.created = created # test to see if any of the name components are set sub_name_set = any(sub_name != "" for sub_name in (first_name, middle_name, last_name, suffix)) # we don't expect full_name and a subset to be set at the same time if full_name != "" and sub_name_set: raise ValueError("You cannot set 'full_name' and a subset of the " "name at the same time.") # set the name via full_name or via the constituent parts if full_name != "": self.full_name = full_name else: self.first_name = first_name.title() self.last_name = last_name.title() self.middle_name = middle_name.title() self.suffix = suffix.upper() if (donations is not None) and (donation is not None): raise ValueError("You cannot set 'donations' and 'donation' at " "the same time.") if donation is not None: # if a donation is passed in, initialize the donations, # then add the donation self._donations = list() self.add_donation(donation) else: if donations is not None: # TODO donations need more validation self._donations = donations else: self._donations = list() @property def did(self): """ return the donor did """ return self._did @did.setter @audit_log def did(self, value): """ set the did """ self._did = value @property def donations(self): """ print all donations """ return self._donations @donations.setter @audit_log def donations(self, value): """ allow bulk setting of donation entries """ self._donations = value @audit_log def add_donation(self, value): """ add a single donation """ today = datetime.datetime.utcnow().date().isoformat() + "Z" try: value = float(value) except ValueError: raise ValueError("Donations must be values.") self._donations.append({"amount": value, "date": today}) @property def number_donations(self): """ return the number of donations """ return len(self._donations) @property def total_donations(self): """ return a sum of the donations """ total_donations = 0 for donation in self.donations: total_donations += donation["amount"] return round(total_donations, 2) @property def average_donations(self): """ return the average donation amount """ try: return round(self.total_donations / self.number_donations, 2) except ZeroDivisionError: return 0 @property def first_name(self): """ return the first name """ return self._first_name @first_name.setter @audit_log def first_name(self, value): """ set the first name """ self._first_name = value.title() @property def middle_name(self): """ return the middle name """ return self._middle_name @middle_name.setter @audit_log def middle_name(self, value): """ set the middle name """ self._middle_name = value.title() @property def last_name(self): """ return the last name """ return self._last_name @last_name.setter @audit_log def last_name(self, value): """ set the last name """ self._last_name = value.title() @property def suffix(self): """ return the suffix """ return self._suffix @suffix.setter @audit_log def suffix(self, value): """ set the suffix """ # special case roman numbers to all caps if value.lower() in ["i", "ii", "iii", "iv", "v"]: self._suffix = value.upper() else: self._suffix = value.title() @property def informal_name(self): """ return full name minus the suffix """ return " ".join( filter(None, [self.first_name, self.middle_name, self.last_name])) @property def full_name(self): """ return the formal, full name """ # if suffix, add with a comma if self.suffix != "": suffix = ", " + self.suffix else: suffix = "" return " ".join( filter(None, [self.first_name, self.middle_name, self.last_name ])) + suffix @full_name.setter def full_name(self, full_name): """ allow the donor information to be updated via full_name as well """ # for simplicity's sake, we make the assumption a suffix will # follow a comma # capture suffix, if present suffix = "" if full_name.find(","): try: suffix = full_name.split(",")[1].strip() except Exception: pass # name with suffix removed informal_name = full_name.split(",")[0].strip() # we assume the last name is one word minus the suffix last_name = informal_name.split()[-1] # build a list with everything to the left of the last name everything_but_last = informal_name.split()[0:-1] # pull the first name off try: first_name = everything_but_last.pop(0) except IndexError: # if we get an error, they probably gave us a malformed name first_name = "" try: # pull the middle, if exists middle_name = everything_but_last.pop(0) except IndexError: middle_name = "" self.first_name = first_name self.middle_name = middle_name self.last_name = last_name self.suffix = suffix def _query_attributes(self, which_attributes=(), return_empty=True): """ return list of formatted attribute/value entries """ attributes = [] for attr in which_attributes: # for our purposes, never print internal attributes if not attr.startswith("_"): try: str_val = repr(getattr(self, attr)) # don't return empty values if they don't want them if eval(str_val) or return_empty: attributes.append(attr + "=" + str_val) except (AttributeError, SyntaxError): # we know certain attributes are not readable, so skip them pass return attributes def __repr__(self): """ Return only the (settable) attributes Return only settable attributes so eval(repr(Donor(<foo>))) == Donor(<foo>) """ which_attributes = [ "did", "first_name", "middle_name", "last_name", "suffix", "donations", "created" ] attributes = self._query_attributes(which_attributes, return_empty=False) return "Donor( " + ", ".join(attributes) + " )" def __str__(self): """ return all the non-internal attributes """ which_attributes = [] for attr in dir(self): if not attr.startswith("_"): which_attributes.append(attr) return "str(Donor(\n " + ",\n ".join( self._query_attributes(which_attributes)) + " ))"