Fixing imports.
[public/pyceo-broken.git] / ceo / urwid / library.py
index 47cba64..ba4a15e 100644 (file)
@@ -3,288 +3,331 @@ 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, timedelta
+from ceo.pymazon import PyMazon
+
+from ceo import terms
 
 import ceo.library as lib
 
+CONFIG_FILE = "/etc/csc/library.cf"
+
+cfg = {}
 
+def configure():
+    """
+    Load configuration
+    """
+    cfg_fields = [ "aws_account_key" ]
+    temp_cfg = conf.read(CONFIG_FILE)
+    conf.check_string_fields(CONFIG_FILE, cfg_fields, temp_cfg)
+    cfg.update(temp_cfg)
 
 def library(data):
+    """
+    Create the main menu for the library system.
+    """
     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),
+#        ("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 search_books(data):
+    """
+    Define menus for searching books.
+    """
+    menu = make_menu([
+        ("Overdue Books", overdue_books, None),
+        ("Signed Out Books", outbooks_search, None),
+    ])
+    push_window(menu, "Book Search")
 
-def return_book(data):
-    "should bring up a searchbox of all the guys first"
-    view_books(lib.search(signedout=True))
+def add_book(data):
+    """
+    Add book to library.  Also stab Sapphyre.
+    """
+    push_wizard("Add Book", [BookAddPage])
 
-def search_books(data):
-    push_window(urwid.Filler(SearchPage(), valign='top'), "Search Books")
+def overdue_books(data):
+    """
+    Display a list of all books that are overdue.
+    """
+    oldest = datetime.today() - timedelta(weeks=2)
+    overdue = lib.Signout.select(lib.Signout.q.outdate<oldest)
 
-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")
+    widgets = []
+
+    for s in overdue:
+        widgets.append(urwid.AttrWrap(ButtonText(None, s.book, str(s.book)),
+                                      None, 'selected'))
+        widgets.append(urwid.Divider())
+        
+    push_window(urwid.ListBox(widgets))
+
+    None
+
+def outbooks_search(data):
+    """
+    Display a list of all books that are signed out.
+    """
+    overdue = lib.Signout.select(lib.Signout.q.indate==None)
 
-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'))
+
+    for s in overdue:
+        widgets.append(urwid.AttrWrap(ButtonText(None, s.book, str(s.book)),
+                                      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
+    None
+
 
+def checkout_book(data):
+    """
+    Display the book checkout wizard.
+    """
+    push_wizard("Checkout", [CheckoutPage, BookSearchPage, ConfirmPage])
+
+def return_book(data):
+    """
+    Display the book return wizard.
+    """
+    push_wizard("Checkout", [CheckinPage, ConfirmPage])
 
-class AddBookPage(WizardPanel):
+class BookAddPage(WizardPanel):
+    """
+    Thingy for going on screen to add books.
+    """
     def init_widgets(self):
-        self.author = SingleEdit("Author: ")
-        self.title = SingleEdit("Title: ")
-        self.year = SingleIntEdit("Year(s): ")
+        """
+        Make some widgets.
+        """
+        self.isbn = SingleEdit("ISBN: ")
+        
         self.widgets = [
-            urwid.Text("Add Book"),
+            urwid.Text("Adding New Book"),
             urwid.Divider(),
-            self.author,
-            self.title,
-            self.year,
+            self.isbn
         ]
-    
+
     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
+        """
+        Do black magic.
+        """
+        isbn = self.isbn.get_edit_text()
+
         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")
-            return True
-        lib.add(author, title, year)
-        raise_back()
+            pymazon = PyMazon(cfg["aws_account_key"])
+            book = pymazon.lookup(isbn)
 
+            currents = lib.Book.select(lib.Book.q.isbn==isbn)
+            if len(currents) == 0:
+                lib.Book(isbn=isbn, title=book.title,
+                         year=book.year, publisher=book.publisher)
+            else:
+                sys.stderr.write("Fuck you.\n")
+                set_status("Book already exists, fucker.")
+                
+        except PyMazonError, e:
+            sys.stderr.write("Book not added: " + e.message + "\n")
+            set_status("Amazon thinks this is not a book.  Take it up with them.")
+        
 
-   
-class SearchPage(urwid.WidgetWrap):
+class BookSearchPage(WizardPanel):
     """
-    TODO: need to be able to jump to "search" button quickly; perhaps trap a certain keypress?
+    The page used when searching for books.
     """
-    def __init__(self):
-        self.author = SingleEdit("Author: ")
+    def init_widgets(self):
+        """
+        Initialize the widgets and state variables.
+        """
+        self.search = None
+        self.state["book"] = None
+        self.isbn = SingleEdit("ISBN: ")
         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,
+
+        self.widgets = [
+            urwid.Text("Book Search"),
+            urwid.Text("(Only one field required.)"),
             urwid.Divider(),
-            urwid.Text("String fields are regexes.\nYear is a comma-separated list or a hyphen-separated range")
+            self.isbn,
+            self.title
         ]
-        buttons = urwid.GridFlow([self.ok, self.back], 10, 3, 1, align='right')
-        urwid.WidgetWrap.__init__(self, urwid.Pile([urwid.Pile(widgets), buttons]))        
+
+    def check(self):
+        """
+        Validate input and update state.
+        """
+        if self.state["book"] is None:
+            push_window(SearchPage(self.isbn.get_edit_text(),
+                                   self.title.get_edit_text(),
+                                   None,
+                                   self.state))
+            return True
+        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")
+
+class CheckoutPage(WizardPanel):
+    """
+    The initial page when checking out a book.
+    """
+    def init_widgets(self):
+        """
+        Initialize widgets and set up state.
+
+        user -> the username to sign the book to
+        task -> used for the confirmation dialog
+        """
+        self.state["user"] = "ERROR"
+        self.state["task"] = "sign_out"
+        self.user = LdapWordEdit(csclub_uri, csclub_base, 'uid', "Username: ")
+        
+        self.widgets = [
+            urwid.Text("Book Checkout"),
+            urwid.Divider(),
+            self.user,
+        ]
+
+    def check(self):
+        self.state['user'] = self.user.get_edit_text()
+        if not members.registered(self.state['user'], terms.current()):
+            set_status("User not registered for this term!")
             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)) 
+        return False
 
+class ConfirmPage(WizardPanel):
+    """
+    The confirmation screen when checking-in and checking-out
+    a book.
+    """
+    def init_widgets(self):
+        """
+        Initialize widgets and state.
+
+        task -> used to deterimine the action
+        """
+        self.user = urwid.Text("Username: ")
+        self.book = urwid.Text("Book: ")
 
+        title = ""
+        if self.state["task"] and self.state["task"]=="sign_in":
+            title = "Checkin"
+        else:
+            title = "Checkout"
 
-#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: ")
         self.widgets = [
-            urwid.Text("Checkout A Book"),
+            urwid.Text("Confirm " + title),
             urwid.Divider(),
-            self.book,
             self.user,
+            self.book
         ]
-        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 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")
+
+    def activate(self):
+        """
+        Ensures that correct data is displayed.
+        """
+        self.user.set_text("Username: " + self.state["user"])
+        if self.state["book"]:
+            self.book.set_text("Book: " + self.state["book"].title)
+
+    def check(self):
+        """
+        Generally used for validation, but in this case it does
+        the actual book check-out.
+        """
+        #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.checkout_label.set_text(self._book.signout._repr_())
-            self.checkout_button.set_label("Check In")
+            self.state["book"].sign_out(self.state["user"])
+        pop_window()
+
+        
+class SearchPage(urwid.WidgetWrap):
+    """
+    Displays search results.  Can search on isbn,
+    title, or username (for books that are currently
+    out).
+    """
+    def __init__(self, isbn, title, user, state):
+        """
+        This does the actual search, and sets up the screen
+        when it's done.
+
+        title -> search by (partial) title
+        isbn -> search by (partial) isbn
+        user -> search by username (for checked-out books)
+        """
+        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):
+        """
+        Marks a book for check-in or check-out.
+        """
+        self.state["book"] = book
+        pop_window()
+
+class CheckinPage(WizardPanel):
+    """
+    The initial page to start the check-in widget.
+    """
+    def init_widgets(self):
+        """
+        Throw some widgets on the screen and set up
+        some state.
+
+        book -> The book to check out.
+        user -> Stupid people like books.
+        task -> What are we doing?  (For confirm screen.)
+        """
+        self.state["book"] = None
+        self.state["user"] = "ERROR"
+        self.state["task"] = "sign_in"
+        self.user = LdapWordEdit(csclub_uri, csclub_base, 'uid', "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):
+        """
+        Pushes the search window.
+
+        Should validate usernames.
+        """
+        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