CS21 Lab 11: Classes and Objects

Part 1 due Saturday, April 27, by 11:59pm

Part 2 due Saturday, May 4, by 11:59pm

Goals

The goals for this lab assignment are:

  • Practice defining your own Python classes

  • More practice working with a list of objects

  • More practice programming with file I/O, and menu-driven program

  • More practice with top down design, functions, and incremental implementation and testing

Getting and Working on Lab11 Code

Make sure you are working in the correct directory when you edit and run lab 11 files.

Run update21 to get the starting point code of the next lab assignment. This will create a new ~/cs21/labs/11 directory for your lab work. Edit and run Lab 11 programs from within your cs21/labs/11 directory.

Make sure all programs are saved to your cs21/labs/11 directory! Files outside that directory will not be graded.

$ update21
$ cd ~/cs21/labs/11
$ pwd
/home/username/cs21/labs/11
$ ls
(should see your program files here)

Then edit and run program files in your ~/cs21/labs/11 directory:

$ code filename.py
$ python3 filename.py

Programming Tips

As you write programs, use good programming practices:

  • Use a comment at the top of the file to describe the purpose of the program (see example).

  • All programs should have a main() function (see example).

  • Use variable names that describe the contents of the variables.

  • Write your programs incrementally and test them as you go. This is really crucial to success: don’t write lots of code and then test it all at once! Write a little code, make sure it works, then add some more and test it again.

  • Don’t assume that if your program passes the sample tests we provide that it is completely correct. Come up with your own test cases and verify that the program is producing the right output on them.

  • Avoid writing any lines of code that exceed 80 columns.

    • Always work in a terminal window that is 80 characters wide (resize it to be this wide)

    • In vscode, at the bottom right in the window, there is an indication of both the line and the column of the cursor.

Function Comments

All functions should have a top-level comment! Please see our function example page if you are confused about writing function comments.

Overview

Organizations such as Kickstarter and GoFundMe provide an opportunity for individuals to raise money for a cause, event, charity, etc. by accepting online financial contributions.

They do so by creating a fund, which has a target that is the amount of money they are hoping to raise. Then, individuals can make a donation to the fund, which in some cases may even exceed its target.

In this assignment you will write a program goKickstartMe.py that reads in data about funds and donations from a file, and then allows the user to perform various functions from a menu of options, for instance listing a contributor’s donations to all funds, or making a donation to a fund.

You will complete this lab in two parts:

  • In Part 1, you will define Python classes to represent a donation and a fund, and write a function to read data from a file. This part of the assignment is due Saturday, April 27.

  • In Part 2, you will write a program that uses these classes to perform various operations. This part of the assignment is due Saturday, May 4.

Be sure you read through the entire assignment description before starting!

1. Part 1. Donation and Fund Classes; Reading from a File (due April 27)

Start your program by creating two Python classes as defined below, and then writing code to populate a list. Be sure you test the various functions in these classes before proceeding to Part 2.

1.1. Donation Class

The Donation class represents a single donation to one of the funds and is defined in donation.py.

1.1.1. Instance Variables

Complete the implementation of the Donation class so that it contains the following data (or "instance variables"):

  • contributor (string) - the name of the person who made the donation

  • date (string) - the date on which the donation was made

  • amount (float) - the amount of money that was donated

1.1.2. Methods

The Donation class should define the following methods:

  • __init__ - the constructor that initializes the values for contributor, date, and amount, in that order

  • getContributor - returns the value of the contributor instance variable

  • getDate - returns the value of the date instance variable

  • getAmount - returns the value of the amount instance variable

  • __str__ - returns a string that contains the contributor, date, and amount, e.g. "Bluey donated $25.32 on 2024-04-05".

    • Note that the amount should be displayed to two digits of precision and should have a dollar sign in front of it.

1.1.3. Testing Your Implementation

The code we have provided in donation.py has a main function that provides a simple test for your Donation class implementation:

def main():
    donation = Donation("Bluey", "2024-04-05", 25.32)
    print(donation.getContributor())
    print(donation)

if __name__ == '__main__':
    main()

When you run your code, you should see the following:

$ python3 donation.py
Bluey
Bluey donated $25.32 on 2024-04-05

Modify the main function so that it:

  • prints the result of calling donation.getDate(); it should print "2024-04-05"

  • prints the result of calling donation.getAmount(); it should print 25.32

  • creates a second Donation object with different values, then displays the result of calling getContributor(), getDate(), and getAmount() on that object; those values should all be correct, based on the values you used to create the object

Be sure your Donation class is working correctly before proceeding!

1.2. Fund Class

The Fund class represents a fund to which contributors can donate and is defined in the file fund.py.

1.2.1. Instance Variables

Complete the implementation of the Fund class so that it contains the following instance variables:

  • name (string) - a short name or title for this fund

  • target (float) - the amount of money that the fund is trying to raise

  • total (float) - the total amount of money that has been donated to the fund so far

  • donations (list of Donation objects) - a list containing each of the individual donations

1.2.2. Methods

The Fund class should define the following methods:

  • __init__ - the constructor that initializes the values for name and target, in that order. The constructor should initialize total to 0 and should set donations to an empty list; these should not be arguments to the constructor.

  • getName - returns the value of the name instance variable

  • getTarget - returns the value of the target instance variable

  • getTotal - returns the value of the total instance variable

  • getDonations - returns the value of the donations instance variable, i.e. the entire list of Donation objects

  • __str__ - returns a string that contains the name, target, and total, e.g. "Toys; target: $50.00; current total: $22.50 (45% reached)".

    • Note that the target and total should be displayed to two digits of precision and should have a dollar sign in front of them

    • Also note that the string should contain the percentage of the target that has been reached, i.e. total / target, shown as an int with a percentage sign after it.

    • Hint! You can display a percentage sign using %% in the template/format string.

The Fund class should also define a donate method as follows:

  • The method should take a contributor parameter as a string, and an amount parameter as a float.

  • If the contributor is an empty string, or if the amount is zero or negative, the method should return False.

  • Otherwise, the method should do the following, in this order:

    • Use the contributor and amount parameters to create a new Donation object. For the Donation object’s date, use str(date.today()), which will create a string representing today’s date.

    • Add the new Donation object to the donations instance variable, i.e. this Fund object’s list of Donations

    • Use the amount to update the Fund object’s total

    • Return True

1.2.3. Testing Your Implementation

The code we have provided in fund.py has a main function that provides a simple test for your Fund class implementation:

def main():
    fund = Fund("Toys", 50.0)
    print("name: " + fund.getName())
    print("target: %f" % (fund.getTarget()))

if __name__ == '__main__':
    main()

Then you can run the code and should see the following:

$ python3 fund.py
name: Toys
target: 50.000000

Modify the main function so that it:

  • Calls the donate method on the object with a valid contributor (i.e., a non-empty string) and valid amount (i.e., a positive number). The method should return True and fund.getTotal() should show that the total now equals that value.

  • Calls the donate method on the object with a valid contributor but a negative amount. The method should return False and fund.getTotal() should show that the total has not been updated.

  • Calls the donate method on the object with an empty string for the contributor and a positive number for the amount. The method should return False and fund.getTotal() should show that the total has not been updated.

  • Calls print(fund) to check that the __str__ method is producing the correct string.

Be sure your Fund class is working correctly before proceeding!

1.3. Reading in Data

The file goKickstartMe.py contains an initialize function that is called by main.

Implement the initialize function so that it reads from the files containing information about funds and donations, and then creates and returns a list of Fund objects that are populated with their Donations.

Two small input files have been provided for you; their names are listed in the main function. Larger input files are also available and you should test your program with them before submitting Part 2, below.

The first argument to the initialize function is the name of the file that contains information about funds. Each line of the file lists the fund’s name (string) and target (float), separated by a comma, like this:

Toys,50.0
Clothes,250.0
School Supplies,100.0

Your initialize function should open and read this file, create a Fund object for each line, and then add it to the list that this function will return.

You can assume that the file is well-formatted and that each fund has a distinct name.

After reading in the file of funds, your initialize function should read the file that contains information about donations. Each line of the file lists the fund’s name (string), the contributor’s name (string), and the donation amount (float), separated by a comma, like this:

Toys,Bluey,22.50
Clothes,Bingo,20.50
School Supplies,Chilli,15
Toys,Bandit,30.0
Clothes,Bluey,11.0
Clothes,Chilli,25.0
Clothes,Bluey,40.00

Note that the initialize function should not create Donation objects for each line, but rather should use this information to call the donate method on the appropriate Fund object in the list that the function has already created.

Although you can assume that the file is well-formatted, the initialize function should ignore any lines that have bad or missing information. For instance, the last three lines of the file we have provided look like this:

Bad Fund,Bluey,50.0
Toys,,100.0
Toys,Bingo,-40.0

The first of these indicates a fund that does not exist; the next is missing the contributor name; and the last one has a negative donation amount. No Donation objects should be created for these entries (hint! some of this error handling has already been done in your Fund class!).

The initialize function should then return the list of Fund objects that have been created and updated with their donations. You should print these out to make sure that the list has the correct Fund objects, and that each Fund has the correct Donation objects.

1.4. Submitting Your Code for Part 1

When you have completed this part of the assignment, submit your code before the deadline using handin21.

Once you’ve done that, you’re ready to move on to Part 2! You do not need to wait for Instructor feedback or for the Part 1 deadline to pass before completing the rest of the assignment. Let’s do this!

2. Part 2. Managing Funds and Donations (due May 4)

To complete this assignment, you will modify the goKickstartMe program so that it allows the user to view and manage funds and donations, using the classes you created in Part 1.

2.1. Main Menu

After using the initialize function from Part 1 to read in the data files and create a list of Fund objects, the program should show a menu of options as follows:

================== Menu ======================
1: list all funds
2: create a new fund
3: list donations for a fund
4: make a donation
5: list donations for a contributor
6: quit

If the user enters a value between 1 and 5, the program should invoke a function that performs the corresponding feature as described below. If they enter 6, the program should end.

You can assume that the user enters an int, but need to check that it is between 1 and 6. For any other numerical input, the program should display "Invalid selection" and show the menu again.

2.2. Option 1: List All Funds

If the user selects menu option 1, the program should list all of the funds in the funds list.

For instance, if the user selects option 1 right after the program has read the two input files (without creating any new funds), the output should be as follows:

1: Clothes; target: $250.00; current total: $96.50 (38% reached)
2: School Supplies; target: $100.00; current total: $15.00 (15% reached)
3: Toys; target: $50.00; current total: $52.50 (105% reached)

The funds should be enumerated starting with 1 and should be listed in alphabetical order, which means that you will have to sort them. To help you with this, the goKickstartMe.py code has a sort function that implements selection sort; you can use this as a foundation for your solution. Keep in mind that you are sorting the list of Funds using the value returned by each object’s getName() method, so you will need to modify this code to get it to sort the list correctly.

If there are no Fund objects in the list, the program should print "There are no funds".

After listing the funds, the program should then return to the Main Menu.

2.3. Option 2: Create a New Fund

If the user selects menu option 2, the program should ask the user to provide information to create a new fund and then add it to the funds list.

The program must prompt the user to enter the new fund’s name and target value. It should handle illegal values as follows:

  • If the name is an empty string, the program should display "Name cannot be empty" and then continue to prompt the user to enter the name again until they enter a non-empty string.

  • If there is already a fund with the same name, the program should display "A fund with that name already exists" and then continue to prompt the user to enter the name again until they enter name of a fund that doesn’t already exist. Names can be considered case-sensitive, so "Toys" and "TOYS" would be considered different names.

  • If the target value is zero or negative, the program should display "Target value must be positive" and then continue to prompt the user to enter the target again until they enter a positive number.

Once valid name and target values have been read from the user, the program should create a new Fund object and add it to the funds list. It should then return to the Main Menu.

2.4. Option 3: List Donations for a Fund

If the user selects menu option 3, the program should ask the user to select one of the funds, and then should list all of the donations for that fund.

The program should first list the names of the funds in alphabetical order and enumerate them starting with 1 (Hint! reuse parts of your solution to Option 1 for this!).

It should then prompt the user to select one of the funds by its number, i.e. the number on the left of the listing. If the user enters a number less than 1 or larger than the number of funds, the program should display "That is an invalid choice" and then continue to prompt the user to enter the value again until they enter a valid number.

Once a valid value has been read from the user, the program should list the donations for the corresponding Fund object in the funds list.

For instance, if the user selects option 3 right after the program has read the two input files (without creating any new funds or making any donations), the output should be as follows (user input in bold; the donation dates should reflect today’s date):

Here are the funds:
1: Clothes
2: School Supplies
3: Toys

Please select a fund: 4
That is an invalid choice
Please select a fund: 0
That is an invalid choice
Please select a fund: 1

Here are the donations:
Bingo donated $20.50 on 2024-04-05
Bluey donated $11.00 on 2024-04-05
Chilli donated $25.00 on 2024-04-05
Bluey donated $40.00 on 2024-04-05

Note that:

  • The donations can be listed in whatever order they are stored in the Fund object’s donations list: they do not need to be sorted.

  • You should invoke the Donation object’s __str__ function by passing the Donation to the print function, i.e. print(donation) is the same as print(donation.__str__())).

  • If there are no donations in this fund, the program should print "There are no donations for this fund".

After listing the donations, the program should then return to the Main Menu.

2.5. Option 4: Make a Donation

If the user selects menu option 4, the program should ask the user to select one of the funds, and then should allow the user to enter information about a new donation to that fund.

The program should first list the names of the funds in alphabetical order and enumerate them starting with 1, as in Option 3.

It should then prompt the user to select one of the funds by its number, i.e. the number on the left of the listing. If the user enters a number less than 1 or larger than the number of funds, the program should display "That is an invalid choice" and then continue to prompt the user to enter the value again until they enter a valid number.

The program must then prompt the user to enter the contributor’s name and donation amount. It should handle illegal values as follows:

  • If the name is an empty string, the program should display "Name cannot be empty" and then continue to prompt the user to enter the name again until they enter a non-empty string.

  • If the donation amount is zero or negative, the program should display "The donation amount must be positive" and then continue to prompt the user to enter the amount again until they enter a positive number.

Once valid name and amount values have been read from the user, the program should invoke the donate method on the appropriate Fund object from the funds list. If the donate method returns True, it should print "Thank you for your donation!". It should then return to the Main Menu.

2.6. Option 5: List Donations for a Contributor

If the user selects menu option 5, the program should ask the user to enter the name of a contributor, and then list all of that contributor’s donations.

The program should first prompt the user to enter a contributor’s name. If the name is an empty string, the program should display "Name cannot be empty" and then continue to prompt the user to enter the name again until they enter a non-empty string.

The program should then list all of that contributor’s donations for all funds.

For instance, if the user selects option 5 right after the program has read the two input files (without creating any new funds or making any donations), the output should be as follows (user input in bold; the donation dates should reflect today’s date):

Enter the contributor's name: Bluey

Here are the donations:
$22.50 to Toys on 2024-04-05
$11.00 to Clothes on 2024-04-05
$40.00 to Clothes on 2024-04-05
Bluey has made 3 donations for a total of $73.50

Note that:

  • You do not need to display the donations in any particular order.

  • For each donation, you should indicate the donation amount, the name of the fund, and the donation date.

After displaying the individual donations, the program should display the number of donations and the total amount for that contributor. If the contributor has not made any donations, the program should display "The contributor has not made any donations". It should then return to the Main Menu.

2.7. Quitting the Program

If the user enters 6 at the Main Menu prompt, the program should print "Good bye!" and end.

2.8. Testing with Large Input Files

Before you submit your code, be sure that you are able to run the entire program and use all the features with the two large input files that we have provided.

Modify your main() function so that you set the names of the funds_file and donations_file accordingly:

funds_file = "/usr/local/doc/funds.txt"
donations_file = "/usr/local/doc/donations.txt"

Then run the program and try out all of the menu options.

The link below is output from an example run of a working program that chooses each menu option, some more than one time. Be sure your code works similarly:

Once you’ve checked your work, you’re ready to submit your code! Congratulations on finishing the final lab of the course!

3. Optional Challenge Question

As an optional, no-credit, just-for-fun (and more practice) challenge, implement the following feature:

If the user selects menu option 7, the program should ask the user to select one of the funds, and then show the aggregate donations by contributor, i.e. the donations to that fund grouped by the contributor.

The program should first list the funds in alphabetical order and enumerate them starting with 1, as in Options 3 and 4.

It should then prompt the user to select one of the funds by its number, i.e. the number on the left of the listing. If the user enters a number less than 1 or larger than the number of funds, the program should display "That is an invalid choice" and then continue to prompt the user to enter the value again until they enter a valid number.

Then, for each contributor who has a made a donation to that fund, the program should list the contributor’s name, the number of donations, and the total amount of all their donations. The output should be organized alphabetically by the contributors' names.

For instance, assume a fund has had the following donations:

  • Tia donated $40

  • Andy donated $25

  • Chris donated $10

  • Andy donated $30

  • Tia donated $50

  • Andy donated $10

The output should then indicate (in this order):

  • Andy has made 3 donations for a total of $65

  • Chris has made 1 donation for a total of $10

  • Tia has made 2 donations for a total of $90

If the fund has not had any donations, the program should display "There are no donations to this fund".

After displaying the output, the program should then return to the Main Menu.

General Hints/Tips

  • Use what you know about good Top-Down Design and incremental implementation and testing to implement the program for Part 2. Do not try to complete it all in one sitting! Rather, implement and test each piece a little bit at a time.

  • Refer to in-class code for examples. You likely need to refer to code from many different weeks depending on the example you are looking for, as we’ve covered functions, while loops, formatted output, file I/0, searching, sorting, strings, lists, etc. over many weeks.

  • Use the print() function to add debug statements to help you see what your program is doing as you try to find and fix bugs (and be sure to remove these after you fix the bugs, though!).

  • You can reuse a lot of functions that you wrote in previous labs, e.g. for getting inputs in a certain range, and use other functions as starting points for implementing similar functionality in this assignment.

Answer the Questionnaire

After each lab, please complete the short Google Forms questionnaire. Please select the right lab number (Lab 11) from the dropdown menu on the first question.

Once you’re done with that, you should run handin21 again.

Submitting lab assignments

Remember to run handin21 to turn in your lab files! You may run handin21 as many times as you want. Each time it will turn in any new work. We recommend running handin21 after you complete each program or after you complete significant work on any one program.

Logging out

When you’re done working in the lab, you should log out of the computer you’re using.

First quit any applications you are running, including your vscode editor, the browser and the terminal. Then click on the logout icon (logout icon or other logout icon) and choose "log out".

If you plan to leave the lab for just a few minutes, you do not need to log out. It is, however, a good idea to lock your machine while you are gone. You can lock your screen by clicking on the lock xlock icon. PLEASE do not leave a session locked for a long period of time. Power may go out, someone might reboot the machine, etc. You don’t want to lose any work!