CIT 591 Assignment 6: Library
Fall 2014, 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 patron, we don't create some kind of a "library_card object" and give it to a patron object; the implementation will achieve the same result by simply recording the patron's name in a dictionary. The visible behavior is the same: A person who has been "issued a library card" will be 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

class Calendar(object)
We need to deal with the passage of time, so we need to keep track of dates. Python has a perfectly good date class, but to keep things simple, we will just use an integer to keep track of how many days have passed. There should be one and only one calendar. The first time the librarian opens the library, it should be day 1.
class Book(object)
A book has these attributes (instance variables): title, author, and due_date. The due date is None if the book is not checked out.
class Patron(object)
A patron is a "customer" of the library, and has the attributes name and books_checked_out (a set of Book objects). A patron must "have a library card" in order to check out books (in our program we won't use actual cards, we'll just keep a set of valid patrons.) A patron may have no more than three books checked out at any time.
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. Every method should return a string to be printed by the main method.
__init__(self)
Constructs the Library object (you should create exactly one of these). It should create a global Calendar object, and set the following instance variables:
  • is_open -- initially False.
  • patrons -- a set of patrons.
  • patron_being_served -- initially None.
  • response -- used to accumulate messages for later printing by main.
  • collection -- a list of all books belonging to the library, initially empty. __init__ should not read in the book collection. This should be done by a separate method, read_book_collection(self), which should be called in main immediately after creating the Library.
open(self)
If the library is already open, returns the string "The library is already open!". Otherwise, starts the day by advancing the Calendar, and setting the is_open flag.
Returns: The string "Today is day n."
list_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.".
Possible Exception: "The library is not open.".
Testing: As this method may return a very long string, it will be sufficient to test that you get the correct message when no books are overdue, and a different message (any different messge) when some books are overdue. Also test that exceptions are raised in the appropriate circumstances.
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)
Sets the patron_being_served instance variable, if the patron has a library card; otherwise, returns "name_of_patron does not have a library card." If the patron does have a library card, the method should return a multiline string containing "Now serving name_of_patron." plus a numbered list of books checked out to that patron, if any.
Possible Exception: "The library is not open."
check_in(self, *book_numbers)
One or more books are being returned by the collection and remove it from the set of books currently checked out to the patron. The book_numbers are taken from the list printed by the serve command. 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 number."
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.)

If more than ten books are found, limit the returned string to the first ten books, with an indication that there are more.
check_out(self, *book_numbers)
Checks out the book to the patron currently being served (there must be one!), or tells why the operation is not permitted. The book_numbers 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."
 
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 numbered list of books 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 may print out.
  2. search for a book wanted by the patron (unless you already know its book_number from a previous search).
  3. check_out one or more books by the numbers returned from the search command.
  4. If more books are desired, you can do another search.

Sample output

Library command: open()
Today is day 4.

Library command: list_overdue_books()
Dear Amy,
You have the following books:
      DUE: The Moon and the Bonfire, by Cesare Pavese (due today)
  OVERDUE: Moon over Africa, by Pamela Kent (was due on day 3)

Library command: serve("Amy")
Now helping Amy.
Amy has these books:
   1. The Moon and the Bonfire, by Cesare Pavese
   2. Moon over Africa, by Pamela Kent

Library command: check_in(2)
Moon over Africa returned to shelves.

Library command: search("Thomas")
   1. Black Sunday, by Thomas Harris
   2. Buddenbrooks, by Thomas Mann
   3. Camp Concentration, by Thomas M Disch
   4. Confederates, by Thomas Keneally
   5. Death in Venice, by Thomas Mann
   6. Extinction, by Thomas Bernhard
   7. Far From the Madding Crowd, by Thomas Hardy
   8. Gravity's Rainbow, by Thomas Pynchon
   9. Headlong Hall, by Thomas Love Peacock
  10. Jude the Obscure, by Thomas Hardy
    ...and 12 more.

Library command: check_out(7, 10)
Far From the Madding Crowd, checked out to Amy. 
Jude the Obscure, checked out to Amy.

Library command: check_out(8)
Sorry, Amy already has 3 books checked out.

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().

Most Library methods produce messages. When running the program normally, these messages should be printed. However, when these methods are called from unit test methods, they should not print anything. There are various ways to solve this problem; here are some.

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_numbers is a tuple of all the numbers (zero or more) that you gave it.

Changes to the provided main method:

def main():
    library = Library()
    library.read_book_collection()
    print len(library.collection), 'books in collection.'
    print "Ready for input. Type 'help()' for a list of commands."
    command = '\0'
    while command != 'quit()':
        try:
            command = raw_input('\nLibrary command: ').strip()
            if len(command) == 0:
                print "What? Speak up!\n"
            else:                
                print eval('library.' + command)
                # or
                eval('library.' + command)
                print library.response
                library.response = ''
        except AttributeError, e:
            print "Sorry, I didn't understand:", command
            print "Type 'help()' for a list of the things I do understand.\n"
        except RuntimeError, e:
            print e
        except Exception, e:
            print "Unexpected error:"
            print e # Delete this line after debugging your program

Files

library.py, library_test.py, collection.txt.

Due date

Your program is due before