diff --git a/ceo/library.py b/ceo/library.py index 7bb63de..0f2d9cb 100644 --- a/ceo/library.py +++ b/ceo/library.py @@ -3,13 +3,18 @@ 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 -#LIBRARY_DB = "/users/ceo/library/books.db" +#LIBRARY_DB = "/users/office/library/books.db" LIBRARY_DB = "./csc_library.db" #testing location def format_maybe(v): @@ -20,17 +25,19 @@ def format_maybe(v): return str(v) class Book: - def __init__(self, author, title, year): + def __init__(self, author, title, year, ISBN=None, description=""): """Any of these may be None to indicate 'unknown'.""" - self.author =author + 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, b) + 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 @@ -69,11 +76,11 @@ def reset(): def add(author, title, year): db = shelve.open(LIBRARY_DB,'c') #use w here (not c) to ensure a crash if the DB file got erased (is this a good idea?) - i = len(db) - db[str(i)] = Book(author, title, year) + isbn = str(len(db)) #not true, but works for now + db[isbn] = Book(author, title, year, isbn) db.close() -def search(author=None, title=None, year=None, signedout=None): +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) @@ -89,7 +96,7 @@ def search(author=None, title=None, year=None, signedout=None): """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_signedout = True #default to true (in case of None i.e. this doesn't 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 @@ -105,17 +112,38 @@ def search(author=None, title=None, year=None, signedout=None): 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_signedout + 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 i,book + yield book db.close() +def save(book): + db = shelve.open(LIBRARY_DB, "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(LIBRARY_DB, "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 @@ -130,4 +158,4 @@ if __name__ == '__main__': print "Listing database" for b in search(): #b.sign_out("nguenthe") - print b + print b diff --git a/ceo/urwid/library.py b/ceo/urwid/library.py index 12b0405..0ecbe7c 100644 --- a/ceo/urwid/library.py +++ b/ceo/urwid/library.py @@ -6,36 +6,37 @@ from ceo.urwid.window import * import ceo.library as lib + + def library(data): menu = make_menu([ ("Checkout Book", checkout_book, None), ("Return Book", return_book, None), - ("List Books", search_books, None), + ("Search Books", search_books, None), ("Add Book", add_book, None), - ("Remove Book", remove_book, None), + #("Remove Book", remove_book, None), ("Back", raise_back, None), ]) push_window(menu, "Library") def checkout_book(data): "should only search signed in books" - pass + view_books(lib.search(signedout=False)) def return_book(data): "should bring up a searchbox of all the guys first" - pass + view_books(lib.search(signedout=True)) def search_books(data): - push_wizard("Search Books", [ - SearchPage, - ]) + 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(BookPage(book), "Book detail") + 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')) @@ -45,13 +46,13 @@ def view_books(books): def add_book(data): push_wizard("Add Book", [AddBookPage]) -def remove_book(data): - pass - +#def remove_book(data): +# pass 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("-") @@ -64,7 +65,7 @@ def parse_commaranges(s): l = [] for y in s.split(","): - l.append(numbers(y)) + l.extend(numbers(y)) return l @@ -103,22 +104,35 @@ class AddBookPage(WizardPanel): -class SearchPage(WizardPanel): - def init_widgets(self): +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.signedout = urwid.CheckBox("Checked Out: ") - self.widgets = [ - urwid.Text("Search Library"), - urwid.Divider(), + 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("Author/Title are regexes.\nYear is a comma-separated list or a hyphen-separated range") + urwid.Text("String fields are regexes.\nYear is a comma-separated list or a hyphen-separated range") ] - def check(self): + buttons = urwid.GridFlow([self.ok, self.back], 10, 3, 1, align='right') + urwid.WidgetWrap.__init__(self, urwid.Pile([urwid.Pile(widgets), buttons])) + + def search(self, *sender): author = self.author.get_edit_text() if author == "": author = None #null it so that searching ignores @@ -131,17 +145,22 @@ class SearchPage(WizardPanel): years = None else: #try to parse the year field - years = parse_commaranges( year ) + 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, signedout)) - + 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 @@ -154,14 +173,17 @@ class CheckoutPage(urwid.WidgetWrap): ] 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) @@ -178,6 +200,7 @@ class InputDialog(urwid.WidgetWrap): self.result = None raise Abort() +#DONTUSE def urwid_input(msg): #this should be in widgets.py dialog = InputDialog(msg) @@ -185,13 +208,6 @@ def urwid_input(msg): event_loop(urwid.main.ui) #HACK return dialog.result -def do_checkout(book): - "this is temporary to fil lthe gap until we see what we reall need" - username = urwid_input("Username to check out to?") - if username is None: - set_status("Checkout cancelled") - else: - book.sign_out(username) def do_delete(book): if Confirm("Do you wish to delete %r?" % book): @@ -203,16 +219,58 @@ class BookPage(urwid.WidgetWrap): self.author = SingleEdit("Author: ") self.title = SingleEdit("Title: ") self.year = SingleIntEdit("Year: ") - #now need a checkout widget to go down here.. - #and "Delete" - if book.signout is None: - self.checkout = ButtonText(do_checkout, book, "Check Out") - else: - self.checkout = ButtonText(lambda book: book.sign_in(), book, "Check In") - #self.remove = ButtonText(do_delete, book, "Delete") - display = urwid.GridFlow([self.author, self.title, self.year, - #self.checkout, - #self.remove - ], 15, 3, 1, 'left') - urwid.WidgetWrap.__init__(self, self.author) + self.ISBN = urwid.Text("ISBN: ") + self.description = urwid.Edit("Description: ", multiline=True) + self.checkout_label = urwid.Text("") + + 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) + + buttons = urwid.GridFlow([back, self.checkout_button, save, remove], 13, 2, 1, 'center') + display = urwid.Pile([self.author, self.title, self.year, self.ISBN, + self.description, + urwid.Divider(), buttons]) + urwid.WidgetWrap.__init__(self, display) + + self.refresh() + #all these *senders are to allow these to be used as event handlers or just on their own + 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) + 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) + self.checkout_button.set_label("Check In") + + 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() diff --git a/ceo/urwid/widgets.py b/ceo/urwid/widgets.py index 438078c..cf1f31c 100644 --- a/ceo/urwid/widgets.py +++ b/ceo/urwid/widgets.py @@ -2,6 +2,8 @@ import urwid, ldap from ceo.urwid.window import raise_back, push_window import ceo.ldapi as ldapi +#Todo: kill ButtonText because no one uses it except one place and we can probably do that better anyway + csclub_uri = "ldap://ldap1.csclub.uwaterloo.ca/ ldap://ldap2.csclub.uwaterloo.ca" csclub_base = "dc=csclub,dc=uwaterloo,dc=ca" @@ -33,6 +35,15 @@ class ButtonText(urwid.Text): else: return key +#DONTUSE +class CaptionedText(urwid.Text): + def __init__(self, caption, *args, **kwargs): + self.caption = caption + urwid.Text.__init__(self, *args, **kwargs) + def render(self, *args, **kwargs): + self.set_text(self.caption + self.get_text()[0]) + urwid.Text.render(*args, **kwargs) + class SingleEdit(urwid.Edit): def keypress(self, size, key): if key == 'enter':