Checkout and check-in works!

This commit is contained in:
Michael Gregson 2009-01-10 19:41:09 -05:00
parent e334437d6d
commit e3035e1b9a
2 changed files with 204 additions and 422 deletions

View File

@ -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."

View 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 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