diff --git a/ceo/library.py b/ceo/library.py index a29787552..90c4031ef 100644 --- a/ceo/library.py +++ b/ceo/library.py @@ -1,183 +1,97 @@ -""" 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") - - 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 - - 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)) - - - - - - - -def reset(): - """make a fresh database""" - shelve.open(cfg['library_db_path'],'n').close() - - -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() - -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 + """ Load configuration """ - 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 + cfg_fields = [ "library_connect_string" ] - 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] + temp_cfg = conf.read(CONFIG_FILE) + conf.check_string_fields(CONFIG_FILE, cfg_fields, temp_cfg) + cfg.update(temp_cfg) + sqlhub.processConnection = connectionForURI(cfg["library_connect_string"]) -#def delete(....): -# """must think about how to do this one; it'll have to be tied to the DB ID somehow""" -# pass +class Book(SQLObject): + isbn = StringCol() + title = StringCol() + description = StringCol() + year = StringCol() + publisher = StringCol() + authors = SQLRelatedJoin("Author") + signouts = SQLMultipleJoin("Signout") -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 + 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)" + + 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 + ", " + + book = book.strip(", ") + + return book + + +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." diff --git a/ceo/urwid/library.py b/ceo/urwid/library.py index 47cba6462..60fea848b 100644 --- a/ceo/urwid/library.py +++ b/ceo/urwid/library.py @@ -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 search_books(data): + menu = make_menu([ + ("Overdue Books", overdue_books, None), + ]) + push_window(menu, "Book Search") + +def overdue_books(data): + None + def checkout_book(data): - "should only search signed in books" - view_books(lib.search(signedout=False)) + push_wizard("Checkout", [CheckoutPage, BookSearchPage, ConfirmPage]) def return_book(data): - "should bring up a searchbox of all the guys first" - view_books(lib.search(signedout=True)) + push_wizard("Checkout", [CheckinPage, ConfirmPage]) -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 - - -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 - - -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 + def check(self): + self.state['user'] = self.user.get_edit_text() -#DONTUSE -def Confirm(msg): - #this should be in widgets.py - push_window(ConfirmDialog(msg)) +class ConfirmPage(WizardPanel): + def init_widgets(self): + self.user = urwid.Text("Username: ") + self.book = urwid.Text("Book: ") -#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 do_delete(book): - if Confirm("Do you wish to delete %r?" % book): - lib.delete(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) - - 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() - - 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) - - -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") + title = "" + if self.state["task"] and self.state["task"]=="sign_in": + title = "Checkin" else: - self.checkout_label.set_text(self._book.signout._repr_()) - self.checkout_button.set_label("Check In") + title = "Checkout" + + self.widgets = [ + urwid.Text("Confirm " + title), + urwid.Divider(), + self.user, + self.book + ] + + 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() + - 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() +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: ") + + 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