CIT 591 Assignment 6: Library
Fall 2015, David Matuszek

Purpose of this assignment

General idea of the assignment

You are going to implement software for your local library. The user of the software will be the librarian, who uses your program to issue library cards, check books out to patrons, check books back in, send out overdue notices, and open and close the library.

Library rules:

Details

In a number of methods, we will make a distinction between what the program appears to do (its behavior), and what it actually does (its implementation). For example, when we "issue a library card" to a person, we don't create some kind of a "library_card object"; the program can implement this by creating a Patron object. The visible behavior is the same: A person who has been "issued a library card" will be a Patron who is able to check out books. Don't let yourself be confused by the terminology.

You should not need any additional classes, but if you would like additional methods, feel free to write (and test!) them.

Classes and methods

Create the following classes. All classes should be defined in a file named library.py, and all test classes should be on a separate file named library_test.py.

class Calendar(object)
We need to deal with the passage of time, so we need to keep track of dates. Python has a date class, but it's too complicated for our needs. To keep things simple, we will just use an integer to keep track of how many days have passed. This class needs only a couple of methods:
__init__(self)
The constructor. Sets the date to 0. You should create one and only one Calendar object; time is the same for everyone.
get_date(self)
Returns (as an integer) the current date.
advance(self)
Increment the date (move ahead to the next day), and returns the new date.
class Book(object)
A book has these attributes (instance variables): id, title, author (only one author per book), and due_date. The due date is None if the book is not checked out.
__init__(self, id, title, author)
The constructor. Saves the provided information. When created, the book is not checked out.
get_id(self)
Returns this book's unique identification number.
get_title(self)
Returns this book's title.
get_author(self)
Returns this book's author.
get_due_date(self)
Returns the date (as an integer) that this book is due.
check_out(self, due_date)
Sets the due date of this Book. Doesn't return anything.
check_in(self)
Sets the due date of this Book to None. Doesn't return anything.
__str__(self)
Returns a string of the form "id: title, by author".
class Patron(object)
A patron is a "customer" of the library. A patron must have a library card in order to check out books. A patron with a card may have no more than three books checked out at any time.
__init__(self, name, library)
Constructs a patron with the given name, and no books. The patron must also have a reference to the Library object that he/she uses. So that this patron can later be found by name, have a global dictionary which uses the patron's name as the key and the actual Patron object as the value.
get_name(self)
Returns this patron's name.
check_out(self, book)
Adds this Book object to the set of books checked out by this patron.
give_back(self, book)
Removes this Book object from the set of books checked out by this patron. (Since patrons are usually said to "return" books, I wish I could name this method return, but I hope you can see why I can't do that.)
get_books(self)
Returns the set of Book objects checked out to this patron (may be the empty set, set([])).
get_overdue_books(self)
Returns the set of overdue Book objects checked out to this patron (may be the empty set, set([])).
class Library(object)
This is the "master" class. It has a number of methods which are called by the "librarian" (the user), not by patrons. The librarian does all the work. Since these methods (other than the constructor) will be typed in by the librarian, all the parameters must be strings or numbers, not objects (how would you type in a Book object?). Furthermore, to reassure the librarian that his/her action has worked, all these methods should return a result, for example, "Library card issued to Amy Gutmann."
__init__(self)
Constructs the Library object (you should create exactly one of these).
  • Read in, from the file  collection.txt, a list of (title, author) tuples, Create a Book from each tuple, and save these books in some appropriate data structure of your choice. Give each book a unique id number (starting from 1, not 0). You may have many copies of the "same" book (same title and author), but each will have its own id.
  • Create a Calendar object (you should create exactly one of these).
  • Define an empty dictionary of patrons. The keys will be the names of patrons and the values will be the corresponding Patron objects.
  • Set a flag variable to indicate that the library is not yet open.
  • Sets the current patron (the one being served) to None.
open(self)
If the library is already open, raises an Exception with the message "The library is already open!". Otherwise, starts the day by advancing the Calendar, and setting the flag to indicate that the library is open. ().
Returns: The string "Today is day n."
find_all_overdue_books(self)
Returns a nicely formatted, multiline string, listing the names of patrons who have overdue books, and for each such patron, the books that are overdue. Or, it returns the string "No books are overdue.".
issue_card(self, name_of_patron)
Issues a library card to the person with this name. However, no patron should be permitted to have more than one library card.
Returns either "Library card issued to name_of_patron." or "name_of_patron already has a library card.".
Possible Exception: "The library is not open.".
serve(self, name_of_patron)
Specifies which patron is about to be served (and quits serving the previous patron, if any). The purpose of this method is so that you don't have to type in the person's name again and again for every book that is to be checked in or checked out. What the method should actually do is to look up the patron's name in the dictionary, and save the returned Patron object in an instance variable of this library.
Returns either "Now serving name_of_patron." or "name_of_patron does not have a library card.".
Possible Exception: "The library is not open."
find_overdue_books(self)
Returns a multiline string, each line containing one book (as returned by the book's __str__ method), of the books that have been checked out by the patron currently being served, and which are overdue. If the patron has no overdue books, the value None is returned.
May raise an Exception with an appropriate message:
  • "The library is not open."
  • "No patron is currently being served."
check_in(self, *book_ids)
The books are being returned by the patron currently being served, so return them to the collection and remove them from the set of books currently checked out to the patron. The book_ids are taken from the list returned by the get_books method in the Patron object. Checking in a Book will involve both telling the Book that it is checked in and returning the Book to this library's collection of available Books.
If successful, returns "name_of_patron has returned n books.".
May raise an Exception with an appropriate message:
  • "The library is not open."
  • "No patron is currently being served."
  • "The patron does not have book id."
search(self, string)
Finds those Books whose title or author (or both) contains this string. For example, the string "tact" might return, among other things, the book Contact, by Carl Sagan. The search should be case-insensitive; that is, "saga" would also return this book. Only books which are currently available (not checked out) will be found. If there is more than one copy of a book (with the same title and same author), only one will be found. In addition, to keep from returning too many books, require that the search string be at least 4 characters long.
Returns one of:
  • "No books found."
  • "Search string must contain at least four characters."
  • A multiline string, each line containing one book (as returned by the book's __str__ method.)
 
check_out(self, *book_ids)
Checks out the books to the patron currently being served, or tells why the operation is not permitted. The book_ids could have been found by a recent call to the search method. Checking out a book will involve both telling the book that it is checked out and removing the book from this library's collection of available books.
If successful, returns "n books have been checked out to name_of_patron.".
May raise an Exception with an appropriate message:
  • "The library is not open."
  • "No patron is currently being served."
  • "The library does not have book id."
  • "Patron cannot have more than three books." (added October 8)
 
renew(self, *book_ids)
Renews the books for the patron currently being served (by setting their due dates to today's date plus 7) or tells why the operation is not permitted.
If successful, returns "n books have been renewed for name_of_patron.".
May raise an Exception with an appropriate message:
  • "The library is not open."
  • "No patron is currently being served."
  • "The patron does not have book id."
 
close(self)
Shut down operations and go home for the night. None of the other operations (except quit) can be used when the library is closed.
If successful, returns the string "Good night.".
May raise an Exception with the message "The library is not open."
quit(self)
The mayor, citing a budget crisis, has stopped all funding for the library. Can happen at any time. Returns the string "The library is now closed for renovations.".
Checking books in and out is slightly complex, so here is what the librarian needs to know:
To check books in:
     1. If you have not already done so, serve the patron. This will print a list of books (with id numbers) checked out to that patron.
     2. check_in the books by the numbers given above.
To check books out:
     1. If you have not already done so, serve the patron. You can ignore the list of books that this will print out.
     2. search for a book wanted by the patron (unless you already know its id).
     3. check_out zero or more books by the book ids returned from the search command.
     4. If more books are desired, you can do another search.
You should also have a main function, not in any class, which starts up automatically when you load the program. It should:
Note that the main method is acting as a REPL. The commands it accepts will be space-separated words and numbers. (Hint: Use split()).
Here is the list of available commands:
open
Opens the library. (Calls the Library's open method.)
overdue
Prints a list of overdue books. (Calls find_all_overdue_books.)
card Person's name
Issues a library card to the named person (Creates a Patron.) The person's name may contain spaces.
serve Person's name
Makes a note of which Patron we are serving. (Calls serve.) Also lists the id, title, and author of all books currently checked out to the Patron, and indicates which are overdue.
checkin id numbers
Returns books to the library.
checkout id numbers
Gives books to the patron currently being served. These books cannot be given to any other patron until this patron returns them.
renew id numbers
Extends the period of time that the patron currently being served can keep the books.
search string
Searches for books containing the given string. You can assume that the string does not contain spaces.
close
Closes the library for the day.
quit
Closes the library permanently and quits the program.
Some of these commands may raise exceptions. Any such exception should be caught in main, and the associated message printed. Exceptions should not terminate the program!

Programming details

Instance variables are variables that belong to an object, and instance methods are methods that are defined for an object. Instance variables and methods must be prefixed with self. , when used by the object itself. Local variables (including parameters) and global variables should not be prefixed with self. When an object uses the methods of another object, those methods must be prefixed by a variable that refers to the object; for example, book.get_title().

Three of the methods above, check_out, check_in, and renew, have an asterisk in front of the last parameter. That asterisk is new syntax--it means that, when you call the method, you can give as many book numbers as you please; you don't have to give just one. Inside the method, the parameter book_ids is a tuple of all the numbers (zero or more) that you gave it. To call these methods, you need to either (1) give zero or more integers as arguments, or (2) break up a set or list into separate arguments by prefixing it with an asterisk (for example, library.check_out(*arglist)).

Testing

Many of these methods return strings, and in general it's a nuisance to test for particular strings, because changing the wording of a message slightly won't affect whether the program is working, but it can cause tests to fail. In these cases it will be acceptable to perform simpler tests, such as "card" in result, or even type(result) is str. You should at least test whether the correct type of result (string or None) is being returned, and you should test that all exceptions are raised when they should be.

You can test __init__ methods, not by calling them, but by creating an object and testing whether it was initialized correctly. You can test __str__ methods by calling the str function on an object.

You don't need to test the assorted "getter" methods (those named get_something). Those are so trivial that it's probably safe to assume that they work.

Grading

Zip together and turn in two files: library.py, containing all of the above classes, and library_test.py, containing all the unit tests.

Due date

Your program is due before