Checkout and check-in works!
authorMichael Gregson <mgregson@csclub.uwaterloo.ca>
Sun, 11 Jan 2009 00:41:09 +0000 (19:41 -0500)
committerMichael Gregson <mgregson@csclub.uwaterloo.ca>
Sun, 11 Jan 2009 00:41:09 +0000 (19:41 -0500)
ceo/library.py
ceo/urwid/library.py

index a297875..90c4031 100644 (file)
-""" The backend for the library-tracking system
-
-This uses shelve which is pretty simplistic, but which should be sufficient for our (extremely minimal) use of the library.
-
-There is a booklist (book=(Author, Title, Year, Signout)) where signout is None for "Checked In" or a (userid, date) to give who and when that book was signed out.
-
-We key books by their ISBN number (this is currently only hoboily implemented; we don't use real ISBNs yet)
-
-Future plans: use barcode scanners, index by ISBN, cross reference to library of congress
-Future plans: keep a whole stack of people who have checked it out (the last few at least)
-"""
-
-import shelve
-import time
-import re
+from sqlobject import *
+from sqlobject.sqlbuilder import *
 from ceo import conf
+import time
+from datetime import datetime, timedelta
 
-### Configuration ###
-
-CONFIG_FILE = '/etc/csc/library.cf'
+CONFIG_FILE = "etc/library.cf"
 
 cfg = {}
 
 def configure():
-    """Load Members Configuration"""
-
-    string_fields = [ 'library_db_path' ]
-    numeric_fields = [ ]
-
-    # read configuration file
-    cfg_tmp = conf.read(CONFIG_FILE)
-
-    # verify configuration
-    conf.check_string_fields(CONFIG_FILE, string_fields, cfg_tmp)
-    conf.check_integer_fields(CONFIG_FILE, numeric_fields, cfg_tmp)
-
-    # update the current configuration with the loaded values
-    cfg.update(cfg_tmp)
-
-
-
-def format_maybe(v):
-    """little hack to make printing things that may come out as None nicer"""
-    if v is None:
-        return "unknown"
-    else:
-        return str(v)
-
-class Book:
-    def __init__(self, author, title, year, ISBN=None, description=""):
-        """Any of these may be None to indicate 'unknown'."""
-        self.author = author
-        self.title = title
-        self.year = year
-        self.ISBN = ISBN
-        self.description = description
-        self.signout = None
-    def sign_out(self, username):
-        if self.signout is None:
-            self.signout = Signout(username)
-        else:
-            raise Exception("Book already signed out to %s" % self.signout.name, self)
-    def sign_in(self): 
-        if self.signout is not None:
-            self.signout = None
-        else:
-            raise Exception("Book was not signed out, no need to sign it in")
+    """ Load configuration
+    """
+    cfg_fields = [ "library_connect_string" ]
     
-    def __str__(self):
-        author = self.author
-        book = "%s [%s]\nBy: %s" % (format_maybe(self.title), format_maybe(self.year), format_maybe(self.author))
-        if self.signout:
-            book += "\n Signed out by %s on %s" %  (self.signout.name, time.ctime(self.signout.date))
-        return book
+    temp_cfg = conf.read(CONFIG_FILE)
+    conf.check_string_fields(CONFIG_FILE, cfg_fields, temp_cfg)
+    cfg.update(temp_cfg)
     
-    def __repr__(self):
-        return "Book(author=%r, title=%r, year=%r, signout=%r)" % (self.author, self.title, self.year, self.signout)
-
-class Signout:
-    """Represents a sign-out of a book to someone. Automatically records when the signout occured"""
-    def __init__(self, name):
-        #in theory we could check that the name given to us is in LDAP
-        self.name = str(name)
-        self.date = time.time()
-    def __repr__(self):
-        return "Signout(%r, %s)" % (self.name, time.ctime(self.date))
-
-
-
-
+    sqlhub.processConnection = connectionForURI(cfg["library_connect_string"])
+
+class Book(SQLObject):
+    isbn = StringCol()
+    title = StringCol()
+    description = StringCol()
+    year = StringCol()
+    publisher = StringCol()
+    authors = SQLRelatedJoin("Author")
+    signouts = SQLMultipleJoin("Signout")
+
+    def sign_out(self, u):
+        s = Signout(username=u, book=self,
+                    outdate=datetime.today(), indate=None)
+
+    def sign_in(self, u):
+        s = self.signouts.filter(AND(Signout.q.indate==None, Signout.q.username==u))
+        if s.count() > 0:
+            s.orderBy(Signout.q.outdate).limit(1).getOne(None).sign_in()
+            return True
+        else:
+            raise Exception("PEBKAC:  Book not signed out!")
 
+    def __str__(self):
+        book = "%s [%s]" % (self.title, self.year)
+        book += "\nBy: "
+        for a in self.authors:
+            book += a.name
+            book += ", "
 
+        if self.authors.count() < 1:
+            book += "(unknown)"
 
-def reset():
-    """make a fresh database"""
-    shelve.open(cfg['library_db_path'],'n').close()
+        book = book.strip(", ")
 
+        signouts = self.signouts.filter(Signout.q.indate==None)
+        if signouts.count() > 0:
+            book += "\nSigned Out: "
+            for s in signouts:
+                book += s.username + ", "
 
-def add(author, title, year):
-    db = shelve.open(cfg['library_db_path'],'c') #use w here (not c) to ensure a crash if the DB file got erased (is this a good idea?)
-    isbn = str(len(db)) #not true, but works for now
-    db[isbn] = Book(author, title, year, isbn)
-    db.close()
+        book = book.strip(", ")
+        
+        return book
 
-def search(author=None, title=None, year=None, ISBN=None, description=None, signedout=None):
-    """search for a title
-    author and title are regular expressions
-    year is a single number or a list of numbers (so use range() to search the DB)
-    whichever ones passed in that aren't None are the restrictions used in the search
-    possibly-useful side effect of this design is that search() just gets the list of everything
-    this is extraordinarily inefficient, but whatever (I don't think that without having an indexer run inthe background we can improve this any?)
-    returns: a sequence of Book objects
-    """
-    db = shelve.open(cfg['library_db_path'], 'c', writeback=True) #open it for writing so that changes to books get saved
-    if type(year) == int:
-        year = [year]
-    def filter(book):
-        """filter by the given params, but only apply those that are non-None"""
-        #this code is SOOO bad, someone who has a clear head please fix this
-        #we need to apply:
-        b_auth = b_title = b_year = b_ISBN = b_description = b_signedout = True #default to true (in case of None i.e. this doesn't apply) 
-        if author is not None:
-            if re.search(author, book.author):
-                b_auth = True
-            else:
-                b_auth = False
-        if title is not None:
-            if re.search(title, book.title): #should factor this out 
-                b_title = True
-            else:
-                b_title = False
-        if year is not None: #assume year is a list
-            if book.year in year:
-                b_year = True
-            else:
-                b_year = False
-        if ISBN is not None:
-            if re.search(ISBN, book.ISBN):
-                b_ISBN = True
-            else:
-                b_ISBN = False
-        if description is not None:
-            if re.search(description, book.description):
-                b_description = True
-            else:
-                b_description = False
-        if signedout is not None:
-            b_signedout = signedout == (book.signout is not None)
-        return b_auth and b_title and b_year and b_ISBN and b_description and b_signedout
-    
-    for i in db:
-        book = db[i]
-        if(filter(book)):
-            yield book
-    db.close()
-
-
-def save(book):
-    db = shelve.open(cfg['library_db_path'], "w")
-    assert book.ISBN is not None, "We should really handle this case better, like making an ISBN or something"
-    db[book.ISBN] = book
-    db.close()
-
-def delete(book):
-    db = shelve.open(cfg['library_db_path'], "w")
-    del db[book.ISBN]
-    
 
-#def delete(....):
-#    """must think about how to do this one; it'll have to be tied to the DB ID somehow"""
-#    pass
-
-if __name__ == '__main__':
-    #print "Making database"
-    #reset()
-    #print
-    #print "Filling database"
-    #add("Bob McBob", "My Life Of Crime", None)
-    print
-    print "Listing database"
-    for b in search():
-        #b.sign_out("nguenthe")
-        print b 
+class Author(SQLObject):
+    name = StringCol()
+    books = RelatedJoin("Book")
+
+class Signout(SQLObject):
+    username = StringCol()
+    book = ForeignKey("Book")
+    outdate = DateCol()
+    indate = DateCol()
+
+#     def __init__(self, u, b, o, i):
+#         username = u
+#         book = b
+#         outdate = o
+#         indate = i
+
+    def sign_in(self):
+        self.indate = datetime.today()
+
+    def _get_due_date(self):
+        """
+        Compute the due date of the book based on the sign-out
+        date.
+        """
+        return self.outdate + timedelta(weeks=2)
+
+if __name__ == "__main__":
+    configure()
+    Book.createTable()
+    Author.createTable()
+    Signout.createTable()
+    print "This functionality isn't implemented yet."
index 47cba64..60fea84 100644 (file)
@@ -3,6 +3,8 @@ from ceo import members
 from ceo.urwid import search
 from ceo.urwid.widgets import *
 from ceo.urwid.window import *
+from sqlobject.sqlbuilder import *
+from datetime import datetime
 
 import ceo.library as lib
 
@@ -12,279 +14,145 @@ def library(data):
     menu = make_menu([
         ("Checkout Book", checkout_book, None),
         ("Return Book", return_book, None),
-        ("Search Books", search_books, None),
-        ("Add Book", add_book, None),
-        #("Remove Book", remove_book, None),
+#        ("Search Books", search_books, None),
+#        ("Add Book", add_book, None),
         ("Back", raise_back, None),
     ])
     push_window(menu, "Library")
 
-def checkout_book(data):
-    "should only search signed in books"
-    view_books(lib.search(signedout=False))
-
-def return_book(data):
-    "should bring up a searchbox of all the guys first"
-    view_books(lib.search(signedout=True))
-
 def search_books(data):
-    push_window(urwid.Filler(SearchPage(), valign='top'), "Search Books")
-
-def view_book(book):
-    "this should develop into a full fledged useful panel for doing stuff with books. for now it's not."
-    push_window(urwid.Filler(BookPage(book), valign='top'), "Book detail")
-
-def view_books(books):
-    #XXX should not use a hardcoded 20 in there, should grab the value from the width of the widget
-    #TODO: this should take the search arguments, and stash them away, and everytime you come back to this page it should refresh itself
-    widgets = []
-    for b in books:
-        widgets.append(urwid.AttrWrap(ButtonText(view_book, b, str(b)), None, 'selected'))
-        widgets.append(urwid.Divider())
-    push_window(urwid.ListBox(widgets))
-
-def add_book(data):
-    push_wizard("Add Book", [AddBookPage])
-
-#def remove_book(data):
-#    pass
+    menu = make_menu([
+        ("Overdue Books", overdue_books, None),
+    ])
+    push_window(menu, "Book Search")
 
+def overdue_books(data):
+    None
 
-def parse_commaranges(s):
-    """parse a string into a list of numbers"""
-    """Fixme: this should be in a different module"""
-    def numbers(section):
-        if "-" in section:
-            range_ = section.split("-")
-            assert len(range_) == 2
-            start = int(range_[0])
-            end = int(range_[1])
-            return range(start, end+1) #+1 to be inclusive of end
-        else:
-            return [int(section)]
-    
-    l = []
-    for y in s.split(","):
-        l.extend(numbers(y))
-    return l
+def checkout_book(data):
+    push_wizard("Checkout", [CheckoutPage, BookSearchPage, ConfirmPage])
 
+def return_book(data):
+    push_wizard("Checkout", [CheckinPage, ConfirmPage])
 
-class AddBookPage(WizardPanel):
+class BookSearchPage(WizardPanel):
     def init_widgets(self):
-        self.author = SingleEdit("Author: ")
+        self.search = None
+        self.state["book"] = None
+        self.isbn = SingleEdit("ISBN: ")
         self.title = SingleEdit("Title: ")
-        self.year = SingleIntEdit("Year(s): ")
+
         self.widgets = [
-            urwid.Text("Add Book"),
+            urwid.Text("Book Search"),
+            urwid.Text("(Only one field required.)"),
             urwid.Divider(),
-            self.author,
-            self.title,
-            self.year,
+            self.isbn,
+            self.title
         ]
-    
+
     def check(self):
-        author = self.author.get_edit_text()
-        if author == "":
-            author = None #null it so that searching ignores
-        title = self.title.get_edit_text()
-        if title == "":
-            title = None
-        try:
-            year = self.year.get_edit_text()
-            if year == "":
-                year = None
-            else:
-                year = int(year)
-        except:
-            self.focus_widget(self.year)
-            set_status("Invalid year")
+        if self.state["book"] is None:
+            push_window(SearchPage(self.isbn.get_edit_text(),
+                                   self.title.get_edit_text(),
+                                   None,
+                                   self.state))
             return True
-        lib.add(author, title, year)
-        raise_back()
-
-
-   
-class SearchPage(urwid.WidgetWrap):
-    """
-    TODO: need to be able to jump to "search" button quickly; perhaps trap a certain keypress?
-    """
-    def __init__(self):
-        self.author = SingleEdit("Author: ")
-        self.title = SingleEdit("Title: ")
-        self.year = SingleEdit("Year(s): ")
-        self.ISBN = SingleEdit("ISBN: ")
-        self.description = urwid.Edit("Description: ", multiline=True)
-        self.signedout = urwid.CheckBox(": Checked Out")
-        self.ok = urwid.Button("Search", self.search)
-        self.back = urwid.Button("Back", raise_back)
-        widgets = [
-            #urwid.Text("Search Library"),
-            #urwid.Divider(),
-            self.author,
-            self.title,
-            self.year,
-            self.ISBN,
-            self.description,
-            self.signedout,
-            urwid.Divider(),
-            urwid.Text("String fields are regexes.\nYear is a comma-separated list or a hyphen-separated range")
-        ]
-        buttons = urwid.GridFlow([self.ok, self.back], 10, 3, 1, align='right')
-        urwid.WidgetWrap.__init__(self, urwid.Pile([urwid.Pile(widgets), buttons]))        
+        else:
+            return False
         
-    def search(self, *sender):
-        author = self.author.get_edit_text()
-        if author == "":
-            author = None #null it so that searching ignores
-        title = self.title.get_edit_text()
-        if title == "":
-            title = None
-        try:
-            years = self.year.get_edit_text()
-            if years == "":
-                years = None
-            else:
-                #try to parse the year field
-                years = parse_commaranges( years )
-        except:
-            raise
-            self.focus_widget(self.year)
-            set_status("Invalid year")
-            return True
-        ISBN = self.ISBN.get_edit_text()
-        if ISBN == "": ISBN = None
-        description = self.description.get_edit_text()
-        if description == "": description = None
-        signedout = self.signedout.get_state()
-        view_books(lib.search(author, title, years, ISBN, description, signedout)) 
 
-
-
-#DONTUSE
-class CheckoutPage(urwid.WidgetWrap):
-    def __init__(self, book):
-        self.book = SingleEdit("Book: ") #this needs to be a widget that when you click on it, it takes you to the search_books pane, lets you pick a book, and then drops you back here
-        self.user = SingleEdit("Checkoutee: ")
+class CheckoutPage(WizardPanel):
+    def init_widgets(self):
+        self.state["user"] = "ERROR"
+        self.state["task"] = "sign_out"
+        self.user = SingleEdit("Username: ")
+        
         self.widgets = [
-            urwid.Text("Checkout A Book"),
+            urwid.Text("Book Checkout"),
             urwid.Divider(),
-            self.book,
             self.user,
         ]
-        urwid.WidgetWrap.__init__(self, urwid.Pile(self.widgets))
-
-#DONTUSE
-class ConfirmDialog(urwid.WidgetWrap):
-    def __init__(self, msg):
-        raise NotImplementedError
-
-#DONTUSE
-def Confirm(msg):
-    #this should be in widgets.py
-    push_window(ConfirmDialog(msg))
-
-#DONTUSE
-class InputDialog(urwid.WidgetWrap):
-    def __init__(self, msg=None):
-        msg = urwid.Text(msg)
-        self.input = SingleEdit("")
-        ok = urwid.Button("OK", self.ok)
-        cancel = urwid.Button("Cancel", self.cancel)
-        buttons = urwid.Columns([ok, cancel])
-        display = urwid.Pile([msg, self.input, buttons])
-        urwid.WidgetWrap.__init__(self, display)
-    def ok():
-        self.result = self.input.get_edit_text()
-        raise Abort() #break out of the inner event loop
-    def cancel():
-        self.result = None
-        raise Abort()
-
-#DONTUSE
-def urwid_input(msg):
-    #this should be in widgets.py
-    dialog = InputDialog(msg)
-    push_window(dialog)
-    event_loop(urwid.main.ui) #HACK
-    return dialog.result
 
+    def check(self):
+        self.state['user'] = self.user.get_edit_text()
 
-def do_delete(book):
-    if Confirm("Do you wish to delete %r?" % book):
-        lib.delete(book)
+class ConfirmPage(WizardPanel):
+    def init_widgets(self):
+        self.user = urwid.Text("Username: ")
+        self.book = urwid.Text("Book: ")
 
-class BookPageBase(urwid.WidgetWrap):
-    def __init__(self):
-        self.author = SingleEdit("Author: ")
-        self.title = SingleEdit("Title: ")
-        self.year = SingleIntEdit("Year: ")
-        self.ISBN = urwid.Text("ISBN: ")
-        self.description = urwid.Edit("Description: ", multiline=True)
+        title = ""
+        if self.state["task"] and self.state["task"]=="sign_in":
+            title = "Checkin"
+        else:
+            title = "Checkout"
 
-        buttons = urwid.GridFlow(self._init_buttons(), 13, 2, 1, 'center') 
-        display = urwid.Pile([self.author, self.title, self.year, self.ISBN, self.description,] +
-                                self._init_widgets() +
-                                [urwid.Divider(), buttons])
-        urwid.WidgetWrap.__init__(self, display)
-        self.refresh()
+        self.widgets = [
+            urwid.Text("Confirm " + title),
+            urwid.Divider(),
+            self.user,
+            self.book
+        ]
 
-    def _init_widgets(self):
-        return []
-    def _init_buttons(self):
-        return []
-    def refresh(self, *sender):
-        """update the widgets from the data model"""
-        self.author.set_edit_text(self._book.author)
-        self.title.set_edit_text(self._book.title)
-        self.year.set_edit_text(str(self._book.year))
-        self.ISBN.set_text("ISBN: " + self._book.ISBN)
-        self.description.set_edit_text(self._book.description)
+    def activate(self):
+        self.user.set_text("Username: " + self.state["user"])
+        if self.state["book"]:
+            self.book.set_text("Book: " + self.state["book"].title)
 
+    def check(self):
+        #TODO: Validate user at some point (preferrably user entry screen)
+        if self.state["task"] and self.state["task"]=="sign_in":
+            self.state["book"].sign_in(self.state["user"])
+        else:
+            self.state["book"].sign_out(self.state["user"])
+        pop_window()
 
-class BookPage(BookPageBase):
-    def __init__(self, book):
-        self._book = book
-        BookPageBase.__init__(self)
-    def _init_widgets(self):
-        self.checkout_label = urwid.Text("") 
-        return [self.checkout_label]
-    def _init_buttons(self):
-        save = urwid.Button("Save", self.save)
-        self.checkout_button = urwid.Button("", self.checkout)
-        back = urwid.Button("Back", raise_back)
-        remove = urwid.Button("Delete", self.delete)
-        return [back, self.checkout_button, save, remove]
         
-    #all these *senders are to allow these to be used as event handlers or just on their own
-    def refresh(self, *sender):
-        BookPageBase.refresh(self, *sender)
-        if self._book.signout is None:
-            self.checkout_label.set_text("Checked In")
-            self.checkout_button.set_label("Check Out")
-        else:
-            self.checkout_label.set_text(self._book.signout._repr_())
-            self.checkout_button.set_label("Check In")
+class SearchPage(urwid.WidgetWrap):
+    def __init__(self, isbn, title, user, state):
+        self.state = state
+        books = []
+        widgets = []
+        if not title is None and not title=="":
+            books = lib.Book.select(LIKE(lib.Book.q.title, "%" + title + "%"))
+        elif not isbn is None and not isbn=="":
+            books = lib.Book.select(lib.Book.q.isbn==isbn)
+        elif not user is None and not user=="":
+            st = lib.Signout.select(AND(lib.Signout.q.username==user, lib.Signout.q.indate==None))
+            for s in st:
+                books.append(s.book)
+
+        for b in books:
+            widgets.append(urwid.AttrWrap(ButtonText(self.select, b, str(b)),
+                                          None, 'selected'))
+            widgets.append(urwid.Divider())
+
+        urwid.WidgetWrap.__init__(self, urwid.ListBox(widgets))
+
+    def select(self, book):
+        self.state["book"] = book
+        pop_window()
+
+class CheckinPage(WizardPanel):
+    def init_widgets(self):
+        self.state["book"] = None
+        self.state["user"] = "ERROR"
+        self.state["task"] = "sign_in"
+        self.user = SingleEdit("Username: ")
         
-    def save(self, *sender):
-        self._book.author = self.author.get_edit_text()
-        self._book.title = self.title.get_edit_text()
-        yeartmp = self.year.get_edit_text()
-        if yeartmp is not None: yeartmp = int(yeartmp)
-        self._book.year = yeartmp
-        #self._book.ISBN = .... #no... don't do this...
-        self._book.description = self.description.get_edit_text()
-        lib.save(self._book)
-        self.refresh()
-    
-    def checkout(self, *sender):
-        username = "nguenthe"
-        self._book.sign_out(username)
-        self.save()
-    
-    def checkin(self, *sender):
-        self._book.sign_in()
-        self.save()
-    
-    def delete(self, *sender):
-        lib.delete(self._book)
-        raise_back()
+        self.widgets = [
+            urwid.Text("Book Checkin"),
+            urwid.Divider(),
+            self.user,
+        ]
+
+    def check(self):
+        if self.state["book"] is None:
+            push_window(SearchPage(None,
+                                   None,
+                                   self.user.get_edit_text(),
+                                   self.state))
+            return True
+        else:
+            self.state["user"] = self.user.get_edit_text()
+            return False