Moving code about for packaging
authorJohn Ladan <jladan@uwaterloo.ca>
Thu, 19 Dec 2013 01:31:27 +0000 (20:31 -0500)
committerJohn Ladan <jladan@uwaterloo.ca>
Thu, 19 Dec 2013 01:31:27 +0000 (20:31 -0500)
22 files changed:
book_data.py [deleted file]
browser.py [deleted file]
checkout.py [deleted file]
db_layer.py [deleted file]
error_files/error_search [new file with mode: 0644]
exceptions.py [deleted file]
form.py [deleted file]
help_bar.py [deleted file]
librarian [new file with mode: 0755]
librarian.py [deleted file]
library/__init__.py [new file with mode: 0644]
library/book_data.py [new file with mode: 0644]
library/database.py [new file with mode: 0644]
library/exceptions.py [new file with mode: 0644]
library/interface/__init__.py [new file with mode: 0644]
library/interface/browser.py [new file with mode: 0644]
library/interface/checkout.py [new file with mode: 0644]
library/interface/form.py [new file with mode: 0644]
library/interface/help_bar.py [new file with mode: 0644]
library/permissions.py [new file with mode: 0644]
permissions.py [deleted file]
setup.py [new file with mode: 0644]

diff --git a/book_data.py b/book_data.py
deleted file mode 100644 (file)
index 4ade5dc..0000000
+++ /dev/null
@@ -1,138 +0,0 @@
-try:
-        # For Python 3.0 and later
-            from urllib.request import urlopen,URLError
-except ImportError:
-        # Fall back to Python 2's urllib2
-            from urllib2 import urlopen,URLError
-from json import loads,dumps
-from socket import timeout
-import sys
-
-""" Library Book Type Description:
-The book is a dictionary of the form { string : a, ... }
-
-Keys:
-  required: (ideally)
-    title - Book/Article title
-    publisher - string containing semi-colon separated list eg. "UW Press; CSC, inc."
-    authors - as above. each name is of the form "First Initials. Last" eg. "Calum T. Dalek; Conan T.B. Ladan"
-  optional:
-    subtitle - string
-    edition - integer
-    isbn - integer (it's preferred to use the isbn-13 rather than isbn-10)
-    lccn - integer: library of congress catalogue number
-    publish date - string of date (to make things easier to code/catalogue (won't be stored)
-    publish year - int (this kind of thing will have to be confirmed by cataloguer)
-    publish month - int
-    publish location - like publisher
-    
-    pages - integer - just the number of pages
-    pagination - string eg. "xviii, 1327-1850"
-    weight - string (purely for interest's sake eg. "3lb." or "3 pounds"
-    categories - list of strings?
-"""
-
-
-# look up data from openlibrary.org using isbn
-def openLibrary_isbn(ISBN):
-    isbn = str(ISBN)
-    try:
-        jsondata = urlopen("http://openlibrary.org/api/books"
-                           "?format=json&jscmd=data&bibkeys=ISBN:"+isbn,
-                           timeout=3)
-    except URLError as e:
-        return {'title':e}
-    except timeout:
-        return {'title':'Timeout while connecting to OpenLibrary.org'}
-    openBook = loads(jsondata.read().decode('utf-8'))
-    if "ISBN:"+isbn not in openBook:
-        return {'isbn':isbn,'title':'Book not found'}
-    openBook = openBook["ISBN:"+isbn]
-    # create my custom dict for books with the info we want.
-    book = dict({"isbn" : isbn})
-    book["title"]=openBook["title"]
-    book["authors"]=""
-    if "authors" in openBook:
-        for v in openBook["authors"]:
-            book['authors'] += "; " + v['name']
-        book['authors'] = book['authors'][2:]
-    book["publisher"]=""
-    if "publishers" in openBook:
-        for v in openBook["publishers"]:
-            book["publisher"] += "; " + v['name']
-        book['publisher'] = book['publisher'][2:]
-    if "publish_places" in openBook:
-        book["publish location"]=""
-        for v in openBook["publish_places"]:
-            book["publish location"] += "; " + v['name']
-        book['publish location'] = book['publish location'][2:]
-
-    # for lccn, there maybe be multiple values in the query. I'm just taking
-    # the first, but the full list may be useful
-    if "lccn" in openBook['identifiers']:
-        book["lccn"]=int(openBook['identifiers']['lccn'][0])
-    if "publish_date" in openBook:
-        book['publish date']=openBook['publish_date']
-        #code to pull out year and month (hopefully)
-    if "number_of_pages" in openBook:
-        book["pages"]=openBook["number_of_pages"]
-    if "pagination" in openBook:
-        book["pagination"]=openBook["pagination"]
-    if "weight" in openBook:
-        book["weight"]=openBook["weight"]
-    if "subtitle" in openBook:
-        book["subtitle"]=openBook["subtitle"]
-    return book
-
-# look up data from openlibrary.org using lccn
-def openLibrary_lccn(LCCN):
-    lccn = str(LCCN)
-    try:
-        jsondata = urlopen("http://openlibrary.org/api/books"
-                           "?format=json&jscmd=data&bibkeys=lccn:"+lccn,
-                           timeout=3)
-    except URLError:
-        return {}
-    openBook = loads(jsondata.read().decode('utf-8'))
-    if "lccn:"+lccn not in openBook:
-        return {'lccn':lccn,'title':'Book not found'}
-    openBook = openBook["lccn:"+lccn]
-    # create my custom dict for books with the info we want.
-    book = {"lccn" : lccn}
-    book["title"]=openBook["title"]
-    book["authors"]=""
-    if "authors" in openBook:
-        for v in openBook["authors"]:
-            book['authors'] += "; " + v['name']
-        book['authors'] = book['authors'][2:]
-    book["publisher"]=""
-    if "publishers" in openBook:
-        for v in openBook["publishers"]:
-            book["publisher"] += "; " + v['name']
-        book['publisher'] = book['publisher'][2:]
-    if "publish_places" in openBook:
-        book["publish location"]=""
-        for v in openBook["publish_places"]:
-            book["publish location"] += "; " + v['name']
-        book['publish location'] = book['publish location'][2:]
-
-    # for isbn, there maybe be multiple values in the query. I'm just taking
-    # the first, but the full list may be useful
-    # There are also ISBN's that have non-number values :(
-    if "isbn_10" in openBook['identifiers']:
-        book["isbn"]=openBook['identifiers']['isbn_10'][0]
-    if "isbn_13" in openBook['identifiers']:
-        book["isbn"]=openBook['identifiers']['isbn_13'][0]
-    if "publish_date" in openBook:
-        book['publish date']=openBook['publish_date']
-        #code to pull out year and month (hopefully)
-    if "number_of_pages" in openBook:
-        book["pages"]=openBook["number_of_pages"]
-    if "pagination" in openBook:
-        book["pagination"]=openBook["pagination"]
-    if "weight" in openBook:
-        book["weight"]=openBook["weight"]
-    if "subtitle" in openBook:
-        book["subtitle"]=openBook["subtitle"]
-    return book
-
diff --git a/browser.py b/browser.py
deleted file mode 100644 (file)
index b384728..0000000
+++ /dev/null
@@ -1,561 +0,0 @@
-import sys
-import curses
-import db_layer as db
-from form import BookForm,CategoryForm
-
-class browserWindow:
-    # These are actually class variables, not member variables? :<
-    _default_height=25
-    _default_width=60
-
-    hl=0
-    topline = 0
-    entries = []
-    selected = list()
-    commands = [(' /', 'search'), (' n', 'find next'), (' N', 'find previous'),
-                ('F6', 'Sort Column'), (' q', 'quit')]
-    cs = []
-    # column definitions are in (label, weight, specified width) triples
-    columnDefs = [('something',1,None)]
-    mx = my = 0
-    # for searches
-    last_search = ""
-    found_index = 0
-
-    def __init__(self,window,helpbar, height=0, width=0):
-        if not(height and width):
-            height = browserWindow._default_height
-            width = browserWindow._default_width
-            sys.stderr.write(str(height)+', '+str(width)+'\n')
-        self.w = window
-        self.hb = helpbar
-        self.w.resize(height,width)
-        self.updateGeometry()
-        self.commands = self.cs+self.commands
-
-    def load_data(self, data=[]):
-        self.entries = data
-        self.selected = list(map(lambda x:False, self.entries))
-
-    def sortByColumn(self, col):
-        self.entries.sort(key=lambda k: k.get(col,"")) # key=dict.get(col))
-        self.selected = list(map(lambda x: False, self.selected))
-
-
-    def updateGeometry(self):
-        (self.my,self.mx)=self.w.getmaxyx()
-        self.pageSize = self.my-4
-        self.calcColWidths()
-
-    def calcColWidths(self):
-        total_weights = 0
-        available_space = self.mx - len(self.columnDefs) -2
-        cols = []
-        for label,weight,value in self.columnDefs:
-            if value!=None:
-                available_space -= value
-            else:
-                total_weights+=weight
-
-        for label,weight,value in self.columnDefs:
-            if value!=None:
-                cols.append((label,value))
-            else:
-                cols.append((label,available_space*weight//total_weights))
-        self.columns=cols
-
-    def refresh(self):
-        self.hb.commands = self.commands
-        self.hb.refresh()
-        self.w.box()
-        self.displayHeader()
-        for r in range(0,self.pageSize):
-            self.displayRow(r)
-        self.w.refresh()
-        self.highlight()
-
-    def clear(self):
-        self.w.erase()
-        self.w.refresh()
-
-    def centreChild(self,child):
-        (y,x) = self.w.getbegyx()
-        (r,c) = child.getmaxyx()
-        child.mvwin( y+(self.my-r)//2,x+(self.mx-c)//2)
-
-
-    def displayHeader(self):
-        cursor = 2
-        for header,width in self.columns:
-            self.w.addnstr(1,cursor,header+" "*width,width)
-            self.w.addstr(2,cursor,"-"*width)
-            cursor += width+1
-
-    def displayRow(self,row):
-        if self.topline+row < len(self.entries):
-            entry = self.entries[self.topline+row]
-            cursor = 2
-            self.w.addnstr(row+3, 1, " "*self.mx,self.mx-2)
-            if self.selected[self.topline+row]:
-                self.w.addstr(row+3, 1, "*")
-            else:
-                self.w.addstr(row+3, 1, " ")
-            for k,width in self.columns:
-                if k.lower() in entry:
-                    self.w.addnstr(row+3, cursor,
-                                   str(entry[k.lower()])+" "*width, width)
-                cursor += width+1
-        else:
-            self.w.addstr(row+3,1," "*(self.mx-2))
-
-    def highlight(self):
-        row = self.hl-self.topline+3
-        if row > 1 and row < self.my:
-            self.w.chgat(row,1,self.mx-2,curses.A_REVERSE)
-
-    def unHighlight(self):
-        row = self.hl-self.topline+3
-        if row > 1 and row < self.my:
-            self.w.chgat(row,1,self.mx-2,curses.A_NORMAL)
-
-    def mvHighlight(self,delta):
-        new = self.hl+delta
-        new = max(new,0)
-        new = min(new,len(self.entries)-1)
-        self.unHighlight()
-        self.hl = new
-        self.highlight()
-    
-    def scroll(self,delta):
-        self.unHighlight()
-        self.topline += delta
-        self.topline = min(self.topline,len(self.entries)-1)
-        self.topline = max(self.topline,0)
-        self.refresh()
-
-    def search(self, string):
-        case_sensitive = not(string.islower())
-        #sys.stderr.write(str(case_sensitive)+'\n')
-        i = 0
-        found = False
-        for e in self.entries:
-            for k,v in e.items():
-                # we or with found to make sure it is never "unfound"
-                if case_sensitive:
-                    found = str(v).find(string) != -1 or found
-                else:
-                    found = str(v).lower().find(string) != -1 or found
-            if found:
-                break
-            i += 1;
-        if found:
-            self.last_search = string
-            self.search_index = i
-            self.case_sensitive = case_sensitive
-            return i
-        else:
-            self.search_index = -1
-            return -1
-
-    def _find_again(self, direction=1):
-        """Find the next match in the entries
-
-        direction = 1 means look ahead
-        direction = -1 means look back
-        """
-        if self.last_search == "" or self.search_index == -1:
-            return -1
-        found = False
-        if direction == 1:
-            last = len(self.entries) -1
-        elif direction == -1:
-            last = 0
-        for i in range(self.hl+direction, last, direction):
-            for k,v in self.entries[i].items():
-                if self.case_sensitive:
-                    found = str(v).find(self.last_search) != -1 or found
-                else:
-                    found = str(v).lower().find(self.last_search) != -1 or found
-            if found:
-                break
-        if found:
-            self.search_index = i
-            return i
-        else:
-            return -1
-
-    def eventLoop(self):
-        self.w.keypad(1)
-        self.refresh()
-
-        ch = self.w.getch()
-        while ch != 27 and ch != 113:
-            ch = self.handleInput(ch)
-            if ch==113:
-                return {}
-            self.w.refresh()
-            ch = self.w.getch()
-            self.hb.refresh()
-
-    def handleInput(self,ch):
-        if ch == curses.KEY_UP or ch == 107 or ch == 16:
-            if self.hl == self.topline:
-                self.scroll(-self.pageSize//2-1)
-            self.mvHighlight(-1)
-        elif ch == curses.KEY_DOWN or ch == 106 or ch == 14:
-            if self.hl == self.topline+self.pageSize-1:
-                self.scroll(+self.pageSize//2+1)
-            self.mvHighlight(+1)
-        elif ch == curses.KEY_PPAGE:
-            self.scroll(-self.pageSize)
-            self.mvHighlight(-self.pageSize)
-        elif ch == curses.KEY_NPAGE:
-            self.scroll(+self.pageSize)
-            self.mvHighlight(+self.pageSize)
-        elif ch == curses.KEY_HOME:
-            self.scroll(-len(self.entries))
-            self.mvHighlight(-len(self.entries))
-        elif ch == curses.KEY_END:
-            self.scroll(len(self.entries))
-            self.mvHighlight(len(self.entries))
-        elif ch == 47: # forward slash
-            string = self.hb.getSearch()
-            hl = self.search(string)
-            if hl != -1:
-                delta = hl - self.hl
-                self.scroll(delta)
-                self.mvHighlight(delta)
-            else:
-                self.hb.display(string+' not found')
-        elif ch == 110: # n
-            hl = self._find_again(+1)
-            if hl != -1:
-                delta = hl - self.hl
-                self.scroll(delta)
-                self.mvHighlight(delta)
-            else:
-                self.hb.display(self.last_search+' not found')
-        elif ch == 78: # N
-            hl = self._find_again(-1)
-            if hl != -1:
-                delta = hl - self.hl
-                self.scroll(delta)
-                self.mvHighlight(delta)
-            else:
-                self.hb.display(self.last_search+' not found')
-        elif ch == 270: # F6 Sorts
-            w = curses.newwin(1,1)
-            cl = columnSelector(w,self.hb,40,20)
-            self.centreChild(w)
-            col = cl.eventLoop()
-            cl.clear()
-            self.sortByColumn(col)
-            self.clear()
-            self.refresh()
-        elif ch == 32:
-            if len(self.selected)>0:
-                self.selected[self.hl] = not self.selected[self.hl]
-            self.displayRow(self.hl-self.topline)
-            self.highlight()
-
-
-
-class trashBrowser(browserWindow):
-    columnDefs = [('ID',0,3),
-                  ('ISBN',0,13),
-                  ('Authors',30,None),
-                  ('Title',60,None)]
-    
-    cs = [(' r', 'restore selected'), (' d', 'delete selected')]
-
-    
-    # redefinable functions
-    def viewSelection(self,book):
-        bookid = book['id']
-        w=curses.newwin(1,1)
-        bf = BookForm(w, self.hb, book, width=self.mx-10)
-        self.centreChild(w)
-        bf.caption='Viewing Book '+str(bookid)
-        bf.blabel='done'
-        bf.event_loop()
-        bf.clear()
-
-    def restoreSelected(self):
-        books = []
-        for sel,book in zip(self.selected, self.entries):
-            if sel:
-                books.append(book)
-        db.restoreBooks(books)
-
-    def delSelected(self):
-        books = []
-        for sel,book in zip(self.selected, self.entries):
-            if sel:
-                books.append(book)
-        db.deleteBooks(books)
-
-    def refreshBooks(self):
-        self.load_data(db.getRemovedBooks())
-
-    def handleInput(self,ch):
-        browserWindow.handleInput(self,ch)
-        if ch == 10:
-            book = self.entries[self.hl]
-            self.viewSelection(book)
-            self.refresh()
-        if ch==114: #restore books
-            count=0
-            for s in self.selected[0:self.hl-1]:
-                if s:
-                    count+=1
-            self.restoreSelected()
-            self.refreshBooks()
-            self.refresh()
-            self.scroll(-count)
-            self.mvHighlight(-count)
-        if ch==100: # delete books
-            count=0
-            for s in self.selected[0:self.hl-1]:
-                if s:
-                    count+=1
-            self.delSelected()
-            self.refreshBooks()
-            self.refresh()
-            self.scroll(-count)
-            self.mvHighlight(-count)
-        return ch
-
-class bookBrowser(browserWindow):
-    columnDefs = [('ID',0,3),
-                  ('ISBN',0,13),
-                  ('Authors',30,None),
-                  ('Title',60,None)]
-    
-    cs = [(' u', 'update'), (' d', 'delete selected')]
-
-    
-    # redefinable functions
-    def updateSelection(self,book):
-        bookid = book['id']
-        
-        w=curses.newwin(1,1)
-        bf = BookForm(w,self.hb,book, width=self.mx-20)
-        self.centreChild(w)
-        bf.caption='Update Book '+str(bookid)
-        bf.blabel='update'
-        newbook = bf.event_loop()
-        if len(newbook)!=0:
-            db.updateBook(newbook,bookid)
-        bf.clear()
-
-    def viewSelection(self,book):
-        bookid = book['id']
-        w=curses.newwin(1,1)
-        bf = BookForm(w,self.hb,book, width=self.mx-20)
-        self.centreChild(w)
-        bf.caption='Viewing Book '+str(bookid)
-        bf.blabel='done'
-        bf.event_loop()
-        bf.clear()
-
-    def categorizeSelection(self,book):
-        w = curses.newwin(1,1)
-        cs = categorySelector(w,self.hb,40,40)
-        self.centreChild(w)
-        cs.book = book
-        cs.refreshCategories()
-        cs.eventLoop()
-        cs.clear()
-    
-    def delSelected(self):
-        books = []
-        for sel,book in zip(self.selected, self.entries):
-            if sel:
-                books.append(book)
-        db.removeBooks(books)
-
-    def refreshBooks(self):
-        self.load_data(db.get_books())
-
-    def refreshBooksInCategory(self,cat):
-        self.load_data(db.getBooksByCategory(cat))
-
-    def handleInput(self,ch):
-        browserWindow.handleInput(self,ch)
-        if ch == 117: #update on 'u'
-            book = self.entries[self.hl]
-            self.updateSelection(book)
-            self.entries[self.hl]=db.get_book(book['id'])
-            self.refresh()
-        elif ch == 10:
-            book = self.entries[self.hl]
-            self.viewSelection(book)
-            self.refresh()
-        elif ch == 99:
-            book = self.entries[self.hl]
-            self.categorizeSelection(book)
-            self.refresh()
-        if ch==100:
-            count=0
-            for s in self.selected[0:self.hl-1]:
-                if s:
-                    count+=1
-            self.delSelected()
-            self.refreshBooks()
-            self.refresh()
-            self.scroll(-count)
-            self.mvHighlight(-count)
-        return ch
-
-class categoryBrowser(browserWindow):
-    columnDefs = [('Category',100,None)]
-    cs = [(' a', 'add category'), (' d', 'delete selected')]
-
-    def refreshCategories(self):
-        self.load_data(db.getCategories())
-        self.sortByColumn('category')
-
-    def addCategory(self):
-        w = curses.newwin(1,1,10,10)
-        cf = CategoryForm(w,self.hb)
-        self.centreChild(w)
-        cat = cf.event_loop()
-        db.addCategory(cat)
-        cf.clear()
-
-    def viewCategory(self):
-        w = curses.newwin(3,5)
-        b = bookBrowser(w,self.hb)
-        self.centreChild(w)
-        b.refreshBooksInCategory(self.entries[self.hl])
-        b.eventLoop()
-        b.clear()
-
-    def delSelected(self):
-        categories = []
-        for sel,cat in zip(self.selected, self.entries):
-            if sel:
-                categories.append(cat)
-        db.deleteCategories(categories)
-
-    def handleInput(self,ch):
-        browserWindow.handleInput(self,ch)
-        if ch==97:
-            self.addCategory()
-            self.refreshCategories()
-            self.refresh()
-        if ch ==10:
-            self.viewCategory()
-            self.refresh()
-        if ch==100:
-            count=0
-            for s in self.selected[0:self.hl-1]:
-                if s:
-                    count+=1
-            self.delSelected()
-            self.refreshCategories()
-            self.refresh()
-            self.scroll(-count)
-            self.mvHighlight(-count)
-        return ch
-
-class categorySelector(browserWindow):
-    columnDefs = [('Category',100,None)]
-    cs = [(' a', 'add category'), (' c', 'commit')]
-    book = {'id':''}
-    original=[]
-
-
-    def refreshCategories(self):
-        self.entries = db.getCategories()
-        self.sortByColumn('category')
-        self.refreshSelected()
-
-    def refreshSelected(self):
-        self.original = list(map(lambda x:False, self.entries))
-        cats = db.getBookCategories(self.book)
-        cats.sort()
-        cats.sort(key=lambda k: k.get('category')) # key=dict.get(col))
-        i = 0
-        j = 0
-        for cat in self.entries:
-            if i == len(cats):
-                break
-            if cat['id']==cats[i]['cat_id']:
-                self.original[j] = True;
-                i+=1
-            j+=1
-        self.selected = self.original[:]
-
-
-    def addCategory(self):
-        w = curses.newwin(1,1,10,10)
-        cf = CategoryForm(w,self.hb)
-        self.centreChild(w)
-        cats = cf.event_loop()
-        for c in cats:
-            db.addCategory(c)
-        cf.clear()
-
-    def updateCategories(self):
-        # first removed the deselected ones
-        uncats = []
-        cats = []
-        for old, new, category in zip(self.original, self.selected,
-                                      self.entries):
-            if old and not new:
-                uncats.append(category)
-            if not old and new:
-                cats.append(category)
-        db.uncategorizeBook(self.book, uncats)
-        # add the newly selected categories
-        db.categorizeBook(self.book, cats)
-
-
-    def handleInput(self,ch):
-        browserWindow.handleInput(self,ch)
-        if ch==97:
-            self.addCategory()
-            self.refreshCategories()
-            self.refresh()
-        if ch==99:
-            self.updateCategories()
-            return 113
-
-
-
-class columnSelector(browserWindow):
-    columnDefs = [('Column',100,None)]
-    entries = [
-            {'column': 'id'}, {'column': 'isbn'}, {'column': 'lccn'},
-            {'column': 'title'}, {'column': 'subtitle'}, {'column': 'authors'},
-            {'column': 'edition'}, {'column': 'publisher'}, 
-            {'column': 'publish year'}, {'column': 'publish month'}, 
-            {'column': 'publish location'}, {'column': 'pages'},
-            {'column': 'pagination'}, {'column': 'weight'},
-            {'column': 'last updated'},
-    ]
-
-    def __init__(self,window,helpbar,height=40,width=20):
-        self.selected = [False,False,False,False,False,False,False,
-                         False,False,False,False,False,False,False,False]
-        browserWindow.__init__(self,window,helpbar,height,width)
-
-
-    def eventLoop(self):
-        self.w.keypad(1)
-        self.refresh()
-
-        ch = self.w.getch()
-        while ch != 27 and ch != 113:
-            ch = self.handleInput(ch)
-            if ch==10:
-                col = self.entries[self.hl]
-                return col['column']
-            self.w.refresh()
-            ch = self.w.getch()
-            self.hb.refresh()
-    
-    def handleInput(self,ch):
-        browserWindow.handleInput(self,ch)
-        return ch
diff --git a/checkout.py b/checkout.py
deleted file mode 100644 (file)
index 02227c2..0000000
+++ /dev/null
@@ -1,99 +0,0 @@
-import sys
-import curses
-from form import FormWindow,BookForm
-import db_layer as db
-
-
-class BookIDForm(FormWindow):
-    caption = "Enter the book ID"
-    blabel = "Check"
-    labels = ["Book ID"]
-
-    def _return_values(self):
-        if self._confirm_book():
-            return self.entries[0].value
-        else:
-            return False
-
-    def _confirm_book(self):
-        self.clear()
-        bookid = self.entries[0].value
-        book = db.get_book(bookid)
-        bf = BookForm(self.w, self.hb, book, width=self.mx-10)
-        (y,x) = self.w.getbegyx()
-        (r,c) = self.w.getmaxyx()
-        self.w.mvwin( y+(self.my-r)//2,x+(self.mx-c)//2)
-        bf.caption='Confirm the Book '+str(bookid)
-        bf.blabel='Correct'
-        result = bf.event_loop()
-        bf.clear()
-        if result:
-            return True
-
-
-class UWIDForm(FormWindow):
-    caption = "Enter the Patron's username"
-    blabel = "Next"
-    labels = ["Username"]
-
-    def _return_values(self):
-        return self.entries[0].value
-
-class FinalCheck(FormWindow):
-    caption = "Is this information correct?"
-    blabel = "Check Out"
-    labels = ["Username", "Book ID"]
-
-    def _return_values(self):
-        return True
-
-def checkout_procedure(w, hb, cy, cx, mx):
-    """Procedure to check out a book
-
-    w:      ncurses window for the routine
-    cy,cx:  centre coordinates of the screen
-    mx:     max width of screen
-    """
-    # Get the book ID
-    step1 = BookIDForm(w,hb,width=mx-20)
-    (r,c)=w.getmaxyx()
-    w.mvwin(cy-r//2,cx-c//2)
-    book_id = step1.event_loop()
-    step1.clear()
-    if not(book_id):
-        return
-
-    # Get the uwid
-    step2 = UWIDForm(w,hb,width=mx-20)
-    (r,c)=w.getmaxyx()
-    w.mvwin(cy-r//2,cx-c//2)
-    username = step2.event_loop()
-    step2.clear()
-    if not(username):
-        return
-
-    # Confirm the result
-    step3 = FinalCheck(w,hb,book={"username":username,"book id":book_id}, width=mx-20)
-    (r,c)=w.getmaxyx()
-    w.mvwin(cy-r//2,cx-c//2)
-    correct = step3.event_loop()
-    step3.clear()
-    if correct:
-        db.checkout_book(book_id,username)
-
-def return_procedure(w, hb, cy, cx, mx):
-    """Procedure to return a book
-
-    w:      ncurses window for the routine
-    cy,cx:  centre coordinates of the screen
-    mx:     max width of screen
-    """
-    # Get the book ID
-    step1 = BookIDForm(w,hb,width=mx-20)
-    (r,c)=w.getmaxyx()
-    w.mvwin(cy-r//2,cx-c//2)
-    book_id = step1.event_loop()
-    step1.clear()
-    if book_id:
-        db.return_book(book_id)
-    
diff --git a/db_layer.py b/db_layer.py
deleted file mode 100644 (file)
index 6a2b691..0000000
+++ /dev/null
@@ -1,380 +0,0 @@
-import sys
-import sqlite3
-
-import permissions
-
-_catalogue_db_file = '/users/libcom/catalogue.db'
-_book_table = 'books'
-_book_category_table='book_categories'
-_category_table = 'categories'
-
-_checkout_db_file = '/users/libcom/checkout.db'
-_checkout_table = 'checked_out'
-_return_table = 'returned'
-
-_checkout_table_creation = '''
-CREATE TABLE IF NOT EXISTS checked_out
-    (id INTEGER UNIQUE, uwid STRING, date_out DATETIME DEFAULT current_timestamp);
-
-CREATE TABLE IF NOT EXISTS returned
-    (id INTEGER, uwid STRING, date_out DATETIME, date_in DATETIME DEFAULT current_timestamp);
-'''
-
-_book_table_creation = '''
-CREATE TABLE IF NOT EXISTS books
-    (id INTEGER PRIMARY KEY AUTOINCREMENT, 
-     isbn, lccn, title, subtitle, authors, edition, 
-     publisher, publish_year, publish_month, publish_location, 
-     pages, pagination, weight,
-     last_updated DATETIME DEFAULT current_timestamp,
-     deleted BOOLEAN DEFAULT 0);
-
-CREATE TABLE IF NOT EXISTS categories
-    (cat_id INTEGER PRIMARY KEY, category STRING UNIQUE ON CONFLICT IGNORE);
-
-CREATE TABLE IF NOT EXISTS book_categories
-    (id INTEGER, cat_id INTEGER);
-'''
-
-columns = ['id', 'isbn', 'lccn',
-           'title', 'subtitle', 'authors', 'edition', 
-           'publisher', 'publish year', 'publish month', 'publish location', 
-           'pages', 'pagination', 'weight', 'last updated', 'deleted']
-
-_book_trigger_creation = '''
-
-CREATE TRIGGER IF NOT EXISTS update_books_time AFTER UPDATE ON books
-BEGIN
-    UPDATE books SET last_updated = DATETIME('NOW') WHERE rowid = new.rowid;
-END;
-
-CREATE TRIGGER IF NOT EXISTS delete_book AFTER DELETE ON books
-BEGIN
-    DELETE FROM book_categories WHERE id = old.rowid;
-END;
-
-CREATE TRIGGER IF NOT EXISTS delete_category AFTER DELETE ON categories
-BEGIN
-    DELETE FROM book_categories WHERE cat_id = old.cat_id;
-END;
-
-CREATE TRIGGER IF NOT EXISTS insert_book_category_time AFTER INSERT
-ON book_categories
-BEGIN
-    UPDATE books SET last_updated = DATETIME('NOW') WHERE id = new.id;
-END;
-'''
-
-#################################
-# character escaping, etc for sql queries
-#################################
-def _colify(s):
-    return s.replace(" ","_").lower()
-
-def _stringify(v):
-    return '"' + str(v).strip().replace('"','""') + '"'
-
-###################################
-# book functions
-##################################
-@permissions.check_permissions(permissions.PERMISSION_LIBCOM)
-def addBook(book):
-    conn = sqlite3.connect(_catalogue_db_file)
-    c = conn.cursor()
-    cols = []
-    vals = []
-    for k,v in book.items():
-        if v!="":
-            cols.append(_colify(k))
-            vals.append(_stringify(v))
-    
-    query = ("INSERT INTO "+_book_table+" ("+", ".join(cols)+") VALUES ("+
-             ", ".join(vals)+");")
-    c.execute(query)
-    conn.commit()
-    c.close()
-
-@permissions.check_permissions(permissions.PERMISSION_LIBCOM)
-def updateBook(book, bookID):
-    '''
-    Takes book attribute dictionary and a string representating the book ID
-    number, and returns updates the book accordingly
-    '''
-    conn = sqlite3.connect(_catalogue_db_file)
-    c = conn.cursor()
-    updates=[]
-    for k,v in book.items():
-        updates.append(_colify(k)+"="+_stringify(v))
-    query = ("UPDATE "+_book_table+" SET " +  ", ".join(updates)+" WHERE id = " +
-             str(bookID)+";")
-    c.execute(query)
-    conn.commit()
-    c.close()
-
-def get_books():
-    conn = sqlite3.connect(_catalogue_db_file)
-    c = conn.cursor()
-    query = "SELECT * FROM "+_book_table+" WHERE deleted=0;"
-    c.execute(query)
-    books = [_query_to_book(b) for b in c]
-    c.close()
-    return books
-
-def getBooksByCategory(cat):
-    '''
-    Takes a string representating the category ID number, and returns
-    non-deleted books in that category
-    '''
-    conn = sqlite3.connect(_catalogue_db_file)
-    c = conn.cursor()
-    query = ("SELECT "+",".join(map(_colify,columns))+" FROM "+_book_table+
-             " JOIN "+_book_category_table+
-             " USING (id) WHERE cat_id = :id AND deleted=0;")
-    c.execute(query,cat)
-    books = [_query_to_book(b) for b in c]
-    c.close()
-    return books
-
-def getRemovedBooks():
-    conn = sqlite3.connect(_catalogue_db_file)
-    c = conn.cursor()
-    query = "SELECT * FROM "+_book_table+" WHERE DELETED=1;"
-    c.execute(query)
-    books = [_query_to_book(b) for b in c]
-    c.close()
-    return books
-
-def get_book(bookid):
-    conn = sqlite3.connect(_catalogue_db_file)
-    c = conn.cursor()
-    query = "SELECT * FROM "+_book_table+" WHERE id = "+str(bookid)+";"
-    c.execute(query)
-    book = _query_to_book(c.fetchone())
-    c.close()
-    return book
-
-# removes book from catalogue
-@permissions.check_permissions(permissions.PERMISSION_LIBCOM)
-def removeBook(bookid):
-    conn = sqlite3.connect(_catalogue_db_file)
-    c = conn.cursor()
-    query = "UPDATE " +_book_table+ " SET deleted=1 WHERE id = "+str(bookid)+";"
-    c.execute(query)
-    conn.commit()
-    c.close()
-
-@permissions.check_permissions(permissions.PERMISSION_LIBCOM)
-def removeBooks(books):
-    conn = sqlite3.connect(_catalogue_db_file)
-    c = conn.cursor()
-    query1 = "UPDATE " +_book_table+ " SET deleted=1 WHERE id = :id;"
-    for book in books:
-        c.execute(query1, book)
-    conn.commit()
-    c.close()
-
-# restores trashed books
-@permissions.check_permissions(permissions.PERMISSION_LIBCOM)
-def restoreBooks(books):
-    conn = sqlite3.connect(_catalogue_db_file)
-    c = conn.cursor()
-    query1 = "UPDATE " +_book_table+ " SET deleted=0 WHERE id = :id;"
-    for book in books:
-        c.execute(query1,book)
-    conn.commit()
-    c.close()
-
-# fully deletes book from books table
-@permissions.check_permissions(permissions.PERMISSION_LIBCOM)
-def deleteBook(bookid):
-    conn = sqlite3.connect(_catalogue_db_file)
-    c = conn.cursor()
-    query = "DELETE FROM " +_book_table+ " WHERE id = "+str(bookid)+";"
-    c.execute(query)
-    conn.commit()
-    c.close()
-
-@permissions.check_permissions(permissions.PERMISSION_LIBCOM)
-def deleteBooks(books):
-    conn = sqlite3.connect(_catalogue_db_file)
-    c = conn.cursor()
-    query = "DELETE FROM " +_book_table+ " WHERE id = :id;"
-    for book in books:
-        c.execute(query, book)
-    conn.commit()
-    c.close()
-
-def _query_to_book(book_query):
-    # Make a dict out of column name and query results.
-    # Empty entries return None, which are removed from the dict.
-    return dict(filter(lambda t:t[1], zip(columns,book_query)))
-
-def _query_to_book_checkout(book_query):
-    # Make a dict out of column name and query results.
-    # Empty entries return None, which are removed from the dict.
-    b = _query_to_book(book_query)
-    b['uwid'] = book_query[-2]
-    b['date'] = book_query[-1]
-    return b
-
-#########################################
-# Category related functions
-########################################
-def getBookCategories(book):
-    conn = sqlite3.connect(_catalogue_db_file)
-    c = conn.cursor()
-    query = ("SELECT id,cat_id,category FROM "+_book_category_table+" JOIN "+
-             _category_table+" USING (cat_id) WHERE id = :id ;")
-    c.execute(query,book)
-    cats = []
-    for book_id,cat_id,cat_name in c:
-        cats.append({'id':book_id, 'cat_id':cat_id, 'category':cat_name})
-    c.close()
-    return cats
-
-@permissions.check_permissions(permissions.PERMISSION_LIBCOM)
-def categorizeBook(book, cats):
-    conn = sqlite3.connect(_catalogue_db_file)
-    c = conn.cursor()
-    query = ("INSERT OR IGNORE INTO "+_book_category_table+
-             " (id,cat_id) VALUES (?, ?);")
-    for cat in cats:
-        args = (book['id'],cat['id'])
-        c.execute(query,args)
-    conn.commit()
-    c.close()
-
-@permissions.check_permissions(permissions.PERMISSION_LIBCOM)
-def uncategorizeBook(book, cats):
-    conn = sqlite3.connect(_catalogue_db_file)
-    c = conn.cursor()
-    query = "DELETE FROM "+_book_category_table+" WHERE (id = ? AND cat_id = ?);"
-    for cat in cats:
-        args = (book['id'],cat['id'])
-        c.execute(query,args)
-    conn.commit()
-    c.close()
-
-def getCategories():
-    conn = sqlite3.connect(_catalogue_db_file)
-    c = conn.cursor()
-    query = "SELECT cat_id, category FROM "+_category_table+";"
-    c.execute(query)
-    cats = []
-    for cat_id,cat in c:
-        cats.append({'id':cat_id, 'category':cat})
-    c.close()
-    return cats
-
-@permissions.check_permissions(permissions.PERMISSION_LIBCOM)
-def addCategory(cat):
-    conn = sqlite3.connect(_catalogue_db_file)
-    c = conn.cursor()
-    query = ("INSERT OR IGNORE INTO "+_category_table+" (category) VALUES ("
-             +_stringify(cat)+");")
-    c.execute(query)
-    conn.commit()
-    c.close()
-
-@permissions.check_permissions(permissions.PERMISSION_LIBCOM)
-def deleteCategories(cats):
-    conn = sqlite3.connect(_catalogue_db_file)
-    c = conn.cursor()
-    query1 = "DELETE FROM " +_category_table+ " WHERE cat_id = :id;"
-    for cat in cats:
-        c.execute(query1, cat)
-    conn.commit()
-    c.close()
-
-#########################################
-# Book Checkout functions
-#########################################
-@permissions.check_permissions(permissions.PERMISSION_OFFICE)
-def checkout_book(book_id, uwid):
-    conn = sqlite3.connect(_checkout_db_file)
-    c = conn.cursor()
-    query = "INSERT INTO " + _checkout_table + " (id, uwid) VALUES (?, ?);"
-    c.execute(query, (book_id, uwid))
-    conn.commit()
-    c.close()
-
-@permissions.check_permissions(permissions.PERMISSION_OFFICE)
-def return_book(book_id):
-    conn = sqlite3.connect(_checkout_db_file)
-    c = conn.cursor()
-    query = "SELECT uwid,date_out FROM "+ _checkout_table + " WHERE id = :id ;"
-    c.execute(query, {"id": book_id})
-    tmp = c.fetchone()
-    uwid = tmp[0]
-    date_out = tmp[1]
-    query = "INSERT INTO " + _return_table + " (id, uwid, date_out) VALUES (?, ?, ?);"
-    query2 = "DELETE FROM " + _checkout_table + " WHERE id= :id ;"
-    c.execute(query, (book_id, uwid, date_out))
-    c.execute(query2, {"id": book_id});
-    conn.commit()
-    c.close()
-
-def get_checkedout_books():
-    '''
-    retrieves checked out books. The returned books also have the fields
-    uwid: ID of person who signed out the book, and
-    date: date when the book was checked out
-    '''
-    conn = sqlite3.connect(_catalogue_db_file)
-    c = conn.cursor()
-    query = 'ATTACH "' + _checkout_db_file + '" AS co'
-    c.execute(query)
-    query = ("SELECT "+",".join(map(_colify,columns))+",uwid,date_out FROM "+_book_table+
-             " JOIN co."+_checkout_table+
-             " USING (id) ;")
-    c.execute(query)
-    books = [_query_to_book_checkout(b) for b in c]
-    c.close()
-    return books
-
-def get_onshelf_books():
-    '''
-    retrieves checked out books. The returned books also have the fields
-    uwid: ID of person who signed out the book, and
-    date: date when the book was checked out
-    '''
-    conn = sqlite3.connect(_catalogue_db_file)
-    c = conn.cursor()
-    query = 'ATTACH "' + _checkout_db_file + '" AS co'
-    c.execute(query)
-    query = ("SELECT "+",".join(map(_colify,columns))+" FROM "+_book_table+
-             " LEFT JOIN co."+_checkout_table+
-             " USING (id) WHERE uwid ISNULL;")
-    c.execute(query)
-    books = [_query_to_book(b) for b in c]
-    c.close()
-    return books
-
-#########################################
-# Database initialization
-#########################################
-def _createBooksTable():
-    conn = sqlite3.connect(_catalogue_db_file)
-    c = conn.cursor()
-    c.executescript(_book_table_creation)
-    conn.commit()
-    c.close()
-
-def _createTriggers():
-    conn = sqlite3.connect(_catalogue_db_file)
-    c = conn.cursor()
-    c.executescript(_book_trigger_creation)
-    conn.commit()
-    c.close()
-
-def _create_checkout_table():
-    conn = sqlite3.connect(_checkout_db_file)
-    c = conn.cursor()
-    c.executescript(_checkout_table_creation)
-    conn.commit()
-    c.close()
-
-def initializeDatabase():
-    _createBooksTable()
-    _createTriggers()
-    _create_checkout_table()
diff --git a/error_files/error_search b/error_files/error_search
new file mode 100644 (file)
index 0000000..4141183
--- /dev/null
@@ -0,0 +1,20 @@
+Traceback (most recent call last):
+  File "./librarian.py", line 134, in <module>
+    curses.wrapper(menutest, m)
+  File "/usr/lib/python3.2/curses/wrapper.py", line 43, in wrapper
+    return func(stdscr, *args, **kwds)
+  File "./librarian.py", line 29, in menutest
+    menu(w, l)
+  File "./librarian.py", line 66, in menu
+    f()
+  File "./librarian.py", line 115, in browseMenu
+    b.eventLoop()
+  File "/users/jladan/src/library/browser.py", line 180, in eventLoop
+    ch = self.handleInput(ch)
+  File "/users/jladan/src/library/browser.py", line 366, in handleInput
+    browserWindow.handleInput(self,ch)
+  File "/users/jladan/src/library/browser.py", line 203, in handleInput
+    string = self.hb.getSearch()
+  File "/users/jladan/src/library/help_bar.py", line 61, in getSearch
+    string = string + char
+TypeError: Can't convert 'bytes' object to str implicitly
diff --git a/exceptions.py b/exceptions.py
deleted file mode 100644 (file)
index a82df8a..0000000
+++ /dev/null
@@ -1,17 +0,0 @@
-import abc
-
-class LibrarianException(Exception, metaclass=abc.ABCMeta):
-    @abc.abstractproperty
-    def error_msg(self):
-        pass
-
-    def __str__(self):
-        return self.error_msg
-
-class PermissionsError(LibrarianException):
-    def __init__(self, permission_string):
-        self.permission_string = permission_string
-
-    @property
-    def error_msg(self):
-        return "Need privilege level {}".format(self.permission_string)
diff --git a/form.py b/form.py
deleted file mode 100644 (file)
index d084c75..0000000
--- a/form.py
+++ /dev/null
@@ -1,325 +0,0 @@
-import curses
-import sys
-
-
-class TextEntry:
-
-    """A part of a window that handles text entry.
-    Properties:
-        value holds the string that was entered
-    
-    Public Methods:
-        set_geom(row,column,width)  Sets the geometry in the window
-        set_value(string)           Set the value and redraw
-        gain_focus()                Gives it focus, moving cursor and changing the drawing
-        lose_focus()                Takes focus, moving cursor to start, changing drawing
-        handle_input(ch)            Pass this the ncurses key, and it manages input
-        redraw()                    Redraw the text entry (should never need to do this
-        """
-
-    # Public
-    value = ""  # Use the set_value function to set, but retrieve with value
-
-    # Should be Private
-    cursor = 0
-    start = 0
-    focus = False
-    x = 0
-    y = 0
-    width = 10
-
-    # Public methods
-    def __init__(self, parent_window, value=""):
-        self.w = parent_window
-        self.value = value
-
-    def set_geom(self,y,x,width):
-        self.x = x
-        self.y = y
-        self.width = width
-
-    def set_value(self,v):
-        self.value=v
-        self.cursor=len(v)
-        self.redraw()
-
-    def gain_focus(self):
-        #sys.stderr.write('I have focus!\n')
-        self.focus = True
-        self._mv_cursor(+len(self.value))
-        self.start = max(0,self.cursor-self.width) 
-        self.redraw()
-
-    def lose_focus(self):
-        self.focus = False
-        self.cursor = 0
-        self.start = 0
-        self.redraw()
-    
-    def handle_input(self,ch):
-        if ch==curses.KEY_LEFT:
-            self._mv_cursor(-1)
-        elif ch==curses.KEY_HOME:
-            self._set_cursor(0)
-        elif ch==curses.KEY_RIGHT:
-            self._mv_cursor(+1)
-        elif ch==curses.KEY_END:
-            self._set_cursor(len(self.value))
-        elif ch>=32 and ch<=126:
-            self._insert(curses.keyname(ch).decode('utf-8'))
-        elif ch==curses.KEY_BACKSPACE:
-            self._backspace()
-        elif ch==curses.KEY_DC:
-            self._delete()
-
-    def redraw(self):
-        self.w.addnstr(self.y,self.x, self.value[self.start:]+" "*self.width, self.width)
-        if self.focus:
-            self.w.chgat(self.y, self.x, self.width, curses.A_UNDERLINE)
-            curses.curs_set(1)
-
-    # Private functions
-    def _mv_cursor(self,delta):
-        self._set_cursor(self.cursor + delta)
-    
-    def _set_cursor(self, new_c):
-        self.cursor = max(0, min(len(self.value), new_c))
-        self.start = max(0,self.cursor-self.width+1) 
-        self.redraw()
-        # Place the drawn cursor in the correct spot
-        col = self.x + self.cursor - self.start
-        self.w.move(self.y,col)
-
-    def _insert(self,ch):
-        c = self.cursor
-        self.value = self.value[:c] +ch+  self.value[c:]
-        self._mv_cursor(+1)
-
-    def _backspace(self):
-        if self.cursor>0:
-            c = self.cursor
-            self.value=self.value[:c-1] + self.value[c:]
-            self._mv_cursor(-1)
-
-    def _delete(self):
-        c = self.cursor
-        self.value = self.value[:c] + self.value[c+1:]
-        self._mv_cursor(0)
-
-
-
-class FormWindow:
-
-    """General class for a Form Window.
-    
-    To use, make the window for it, call the constructor, then call event_loop.
-    """
-
-    # Private variables
-    mx = my = 0
-    hl = 0
-    bt = -1
-    left = 0
-    top = 2
-    row = 2
-    caption = "Form"
-    blabel = "Done"
-    labels = ["label1"]
-
-    commands = [('pU', 'top'),('pD', 'bottom'),('Es', 'cancel')]
-
-
-    # Public functions
-    def __init__(self,window,helpbar,book={}, width=50):
-        self.w = window
-        self.w.resize(len(self.labels)+6,width)
-        self.hb = helpbar
-        self._make_entries()
-        self._update_geometry()
-        self._set_entries(book)
-
-    def clear(self):
-        self.w.erase()
-        self.w.refresh()
-
-    def event_loop(self):
-        self.w.keypad(1)
-        self.refresh()
-        self.hl=0;
-        self.entries[self.hl].gain_focus()
-
-        ch = self.w.getch()
-        while ch != 27:
-            #sys.stderr.write(curses.keyname(ch).decode('utf-8'))
-            self.handle_input(ch)
-            if ch==10 or ch==curses.KEY_ENTER:
-                if self.bt==0:
-                    return {}
-                elif self.bt==1:
-                    return self._return_values()
-                else:
-                    self._mv_focus(+1)
-            self.w.refresh()
-            ch = self.w.getch()
-        curses.curs_set(0)
-        return {}
-
-    def _make_entries(self):
-        self.entries = []
-        for e in range(len(self.labels)):
-            self.entries.append(TextEntry(self.w))
-
-    def _update_geometry(self):
-        (self.my, self.mx) = self.w.getmaxyx()
-        self.left=0
-        for l in self.labels:
-            self.left = max(len(l),self.left)
-        self.left += 4
-        width = self.mx-self.left-2
-        self.top = 2
-        for r in range(len(self.entries)):
-            self.entries[r].set_geom(r+self.top, self.left, width)
-        # next, the buttons
-        self.brow = self.top+len(self.labels)+1
-        self.bcol = [self.mx-len(self.blabel)-14, self.mx-len(self.blabel)-4]
-        self.bwidth = [8,len(self.blabel)+2]
-
-    def _set_entries(self,book):
-        e = 0
-        for l in self.labels:
-            #sys.stderr.write('updating label: '+l+'\n')
-            if l.lower() in book:
-                #sys.stderr.write('   '+l+' found\n')
-                self.entries[e].value = str(book[l.lower()])
-            else:
-                #sys.stderr.write('   '+l+' notfound\n')
-                self.entries[e].value = ""
-            e += 1 
-
-    def redraw(self):
-        self.w.box()
-        self.w.addstr(0,(self.mx-len(self.caption))//2,self.caption)
-        r=0
-        for l in self.labels:
-            c = self.left-len(l)-2
-            self.w.addstr(r+self.top,c,l+":")
-            self.entries[r].redraw()
-            r+=1
-        self.w.addstr(self.brow,self.bcol[0], "<cancel>  <"+self.blabel+">")
-        self.w.refresh()
-
-    def refresh(self):
-        self.hb.commands = self.commands
-        self.hb.refresh()
-        self._update_geometry()
-        self.redraw()
-
-    def _highlight_button(self):
-        self.w.chgat(self.brow, self.bcol[self.bt], self.bwidth[self.bt], curses.A_REVERSE)
-        curses.curs_set(0)
-
-    def _unhighlight_button(self):
-        self.w.chgat(self.brow,1,self.mx-2,curses.A_NORMAL)
-
-    def _mv_focus(self,delta):
-        if self.bt==-1:
-            self.entries[self.hl].lose_focus()
-        else:
-            self._unhighlight_button()
-        new = self.hl+delta
-        new = max(0, min(len(self.labels), new))   # the extra is for the buttons
-        self.hl = new
-        if new == len(self.labels):
-            self.bt = 1
-            self.bt = min(self.bt,1)
-            self._highlight_button()
-        else:
-            self.bt=-1
-            self.entries[self.hl].gain_focus()
-
-
-    def _return_values(self):
-        book = {}
-        for k,e in zip(self.labels, self.entries):
-            if e!="" and k.lower()!="publish date":
-                book[k.lower()]=e.value
-        return book
-
-
-    def handle_input(self,ch):
-        if ch==curses.KEY_UP:
-            self._mv_focus(-1)
-        elif ch==curses.KEY_PPAGE:
-            self._mv_focus(-len(self.labels))
-        elif ch==curses.KEY_DOWN:
-            self._mv_focus(+1)
-        elif ch==curses.KEY_NPAGE:
-            self._mv_focus(+len(self.labels))
-        elif ch==curses.KEY_LEFT:
-            if self.bt==-1:
-                self.entries[self.hl].handle_input(ch)
-            else:
-                self._unhighlight_button()
-                self.bt=0
-                self._highlight_button()
-        elif ch==curses.KEY_HOME:
-            if self.bt==-1:
-                self._mv_cursor(-len(self.entries[self.hl]))
-        elif ch==curses.KEY_RIGHT:
-            if self.bt==-1:
-                self.entries[self.hl].handle_input(ch)
-            else:
-                self._unhighlight_button()
-                self.bt=1
-                self._highlight_button()
-        else:
-            if self.bt==-1:
-                self.entries[self.hl].handle_input(ch)
-
-        
-        
-
-class BookForm(FormWindow):
-    caption = "Add a Book"
-    blabel = "Add"
-    labels = ["ISBN", "LCCN", "Title", "Subtitle", "Authors", "Edition",
-              "Publisher", "Publish Date", "Publish Year", "Publish Month", "Publish location",
-              "Pages", "Pagination", "Weight"]
-    
-
-    # redefineable functions lookup is called when 'enter' is pressed on ISBN
-    # and returns the looked-up book. Default returns nothing
-    def lookup_isbn(self,isbn):
-        return {'isbn':isbn}
-    
-    def lookup_lccn(self,lccn):
-        return {'lccn':lccn}
-
-    def return_book(self):
-        return self._return_values()
-
-    def handle_input(self,ch):
-        if ch==10 or ch==curses.KEY_ENTER:
-            if self.hl==0:          # lookup by isbn
-                book = self.lookup_isbn(self.entries[0].value)
-                if book != {}:
-                    #sys.stderr.write('updating entries\n')
-                    self._set_entries(book)
-                self.refresh()
-                self._mv_focus(+7)
-            if self.hl==1:          # lookup by lccn
-                book = self.lookup_lccn(self.entries[1].value)
-                if book != {}:
-                    self._set_entries(book)
-                self.refresh()
-                self._mv_focus(+6)
-        else:
-            FormWindow.handle_input(self,ch)
-
-class CategoryForm(FormWindow):
-    caption = "Add a Category"
-    blabel = "Add"
-    labels = ["Category"]
-
-    def _return_values(self):
-        return self.entries[0].value
diff --git a/help_bar.py b/help_bar.py
deleted file mode 100644 (file)
index 880eeac..0000000
+++ /dev/null
@@ -1,70 +0,0 @@
-import curses
-
-class helpBar:
-    # commands is in the form (key, command_name)
-    commands = []
-    mx=my=0
-    x=y=0
-    colWidth = 25
-    numCols=1
-
-    def __init__(self, window):
-        self.w = window
-
-    def updateGeometry(self):
-        (self.my, self.mx) = self.w.getmaxyx()
-        (self.y, self.x) = self.w.getbegyx()
-        self.numCols = self.mx//self.colWidth
-        numRows = len(self.commands)//self.numCols +1
-        self.y += self.my - numRows
-        self.my = numRows
-        self.w.mvwin(0,0)
-        self.w.resize(self.my,self.mx)
-        self.w.mvwin(self.y,self.x)
-
-    def refresh(self):
-        self.clear()
-        self.updateGeometry()
-        r=0
-        c=0
-        for key,command in self.commands:
-            self.w.addnstr(r,c,key+" "+command+" "*self.colWidth,
-                           self.colWidth-1)
-            self.w.chgat(r,c,2,curses.A_REVERSE)
-            c+=self.colWidth
-            if c > self.colWidth*self.numCols:
-                c=0
-                r+=1
-        self.w.refresh()
-
-    def clear(self):
-        self.w.erase()
-        self.w.refresh()
-
-    def getSearch(self):
-        self.clear()
-        self.w.addstr(0,0,"/")
-        string = ""
-        done = False
-        self.w.keypad(1)
-        ch = self.w.getch()
-        while (not done):
-            if ch == curses.KEY_ENTER or ch == 10:
-                return string
-            elif ch == 27: # escape
-                return ""
-            elif ch == curses.KEY_BACKSPACE and string !="":
-                self.w.addstr(0,1," "*len(string))
-                string = string[0:len(string)-1]
-                self.w.addstr(0,1,string)
-            elif ch>=32 and ch<=126:
-                char = curses.keyname(ch).decode('utf-8')
-                string = string + char
-                self.w.addstr(0,1,string)
-            self.w.refresh()
-            ch = self.w.getch()
-
-    def display(self,string):
-        self.clear()
-        self.w.addstr(0,1,string)
-        self.w.refresh()
diff --git a/librarian b/librarian
new file mode 100755 (executable)
index 0000000..7f0f8ca
--- /dev/null
+++ b/librarian
@@ -0,0 +1,185 @@
+#!/usr/bin/env python3
+
+import curses
+import library.database as db
+import library.interface.browser as browser
+import library.interface.form as form
+import library.interface.help_bar as helpBar
+import library.interface.checkout as co
+
+from  library import book_data
+
+
+
+stdscr=0
+hb=0
+
+menu_commands = [(' q','quit')]
+
+def menutest(s, l):
+    global stdscr
+    global hb
+    stdscr=s
+    curses.curs_set(0)
+    (rows,cols)=stdscr.getmaxyx()
+    # set the default for the browser windows
+    browser.browserWindow._default_height = rows-10
+    browser.browserWindow._default_width = cols-10
+    bar = curses.newwin(1,cols-2,rows-1,1)
+    hb = helpBar.helpBar(bar)
+    hb.command=menu_commands
+    hb.refresh()
+    w = curses.newwin(15,40,(rows-10)//2, (cols-40)//2)
+
+    menu(w, l)
+    
+    curses.curs_set(1)
+
+# item is a list of (string, callable) tuples
+def menu(w, items):
+    w.keypad(1)
+    highlight=0
+    redrawMenu(w,items,highlight)
+
+    w.refresh()
+    ch=w.getch()
+    while (ch!=113 and ch!=27): # leave on q or ESC
+        if ch==curses.KEY_UP or ch==107 or ch==16:
+            if highlight!=0:
+                w.chgat(highlight,0, 0)
+                highlight -= 1
+                while(items[highlight][0]==""):
+                    highlight -=1
+                w.chgat(highlight,0, curses.A_REVERSE)
+        if ch==curses.KEY_DOWN or ch==106 or ch==14:
+            if highlight!=len(items)-1:
+                w.chgat(highlight,0, 0)
+                highlight += 1
+                while(items[highlight][0]==""):
+                    highlight +=1
+                w.chgat(highlight,0, curses.A_REVERSE)
+        if ch==curses.KEY_PPAGE:
+            w.chgat(highlight,0, 0)
+            highlight = 0
+            w.chgat(highlight,0, curses.A_REVERSE)
+        if ch==curses.KEY_NPAGE:
+            w.chgat(highlight,0, 0)
+            highlight = len(items)-1
+            w.chgat(highlight,0, curses.A_REVERSE)
+        if ch==114 or ch==10:
+            (s,f)=items[highlight]
+            f()
+            redrawMenu(w,items,highlight)
+        w.refresh()
+        ch = w.getch()
+
+def redrawMenu(w,items,highlight):
+    i=0
+    for (mitem,fun) in items:
+        w.addstr(i,0, mitem)
+        i +=1
+    w.chgat(highlight, 0, curses.A_REVERSE)
+    w.refresh()
+    hb.commands=menu_commands
+    hb.refresh()
+
+
+def addForm():
+    w=curses.newwin(1,1)
+    (my,mx)=stdscr.getmaxyx()
+    bf = form.BookForm(w,hb,width=mx-20)
+    (r,c)=w.getmaxyx()
+    w.mvwin((my-r)//2,(mx-c)//2)
+    bf.lookup_isbn=book_data.openLibrary_isbn
+    bf.lookup_lccn=book_data.openLibrary_lccn
+    bf.caption='Add a Book'
+    bf.blabel = 'Add'
+    book = bf.event_loop()
+    bf.clear()
+    if len(book)!=0:
+        db.addBook(book)
+
+def browseMenu():
+    w=curses.newwin(3,5)
+    b = browser.bookBrowser(w,hb)
+    (r,c) = w.getmaxyx()
+    (my,mx)=stdscr.getmaxyx()
+    w.mvwin((my-r)//2 -2, (mx-c)//2)
+    b.refreshBooks()
+    b.eventLoop()
+    b.clear()
+
+def trashMenu():
+    w=curses.newwin(3,5)
+    b = browser.trashBrowser(w,hb)
+    (r,c) = w.getmaxyx()
+    (my,mx)=stdscr.getmaxyx()
+    w.mvwin((my-r)//2 -2, (mx-c)//2)
+    b.refreshBooks()
+    b.eventLoop()
+    b.clear()
+
+def checkedout_menu():
+    w=curses.newwin(3,5)
+    b = browser.bookBrowser(w,hb)
+    (r,c) = w.getmaxyx()
+    (my,mx)=stdscr.getmaxyx()
+    w.mvwin((my-r)//2 -2, (mx-c)//2)
+    b.load_data(db.get_checkedout_books())
+    b.columnDefs = [("id",0,3),
+                    ("uwid",0,8),
+                    ("date",0,10),
+                    ("title",100,None)]
+    b.calcColWidths()
+    b.eventLoop()
+    b.clear()
+
+def onshelf_menu():
+    w=curses.newwin(3,5)
+    b = browser.bookBrowser(w,hb)
+    (r,c) = w.getmaxyx()
+    (my,mx)=stdscr.getmaxyx()
+    w.mvwin((my-r)//2 -2, (mx-c)//2)
+    b.load_data(db.get_onshelf_books())
+    b.eventLoop()
+    b.clear()
+
+def co_menu():
+    w=curses.newwin(1,1)
+    (my,mx)=stdscr.getmaxyx()
+    co.checkout_procedure(w,hb,my//2,mx//2,mx) 
+
+def return_menu():
+    w=curses.newwin(1,1)
+    (my,mx)=stdscr.getmaxyx()
+    co.return_procedure(w,hb,my//2,mx//2,mx) 
+
+def catMenu():
+    (my,mx)=stdscr.getmaxyx()
+    w=curses.newwin(3,5)
+    cat = browser.categoryBrowser(w,hb, 10,40)
+    (r,c) = w.getmaxyx()
+    w.mvwin((my-r)//2 -2, (mx-c)//2)
+    cat.refreshCategories()
+    cat.sortByColumn('category')
+    cat.eventLoop()
+    cat.clear()
+
+
+if __name__ == "__main__":
+    db.initializeDatabase()
+    m = [("Browse Library", browseMenu),
+         ("Add Book", addForm),
+         ("Categories", catMenu),
+         ("View Trash", trashMenu),
+         ("",exit),
+         ("Check Out a Book", co_menu),
+         ("Return a Book", return_menu),
+         ("",exit),
+         ("View Checked Out Books", checkedout_menu),
+         ("View On Shelf Books", onshelf_menu),
+         ("",exit),
+         ("Exit", exit)]
+    curses.wrapper(menutest, m)
+
+
diff --git a/librarian.py b/librarian.py
deleted file mode 100755 (executable)
index edabe4c..0000000
+++ /dev/null
@@ -1,185 +0,0 @@
-#!/usr/bin/env python3
-
-import curses
-import db_layer as db
-import browser
-import form
-import help_bar as helpBar
-
-import book_data
-
-import checkout as co
-
-
-stdscr=0
-hb=0
-
-menu_commands = [(' q','quit')]
-
-def menutest(s, l):
-    global stdscr
-    global hb
-    stdscr=s
-    curses.curs_set(0)
-    (rows,cols)=stdscr.getmaxyx()
-    # set the default for the browser windows
-    browser.browserWindow._default_height = rows-10
-    browser.browserWindow._default_width = cols-10
-    bar = curses.newwin(1,cols-2,rows-1,1)
-    hb = helpBar.helpBar(bar)
-    hb.command=menu_commands
-    hb.refresh()
-    w = curses.newwin(15,40,(rows-10)//2, (cols-40)//2)
-
-    menu(w, l)
-    
-    curses.curs_set(1)
-
-# item is a list of (string, callable) tuples
-def menu(w, items):
-    w.keypad(1)
-    highlight=0
-    redrawMenu(w,items,highlight)
-
-    w.refresh()
-    ch=w.getch()
-    while (ch!=113 and ch!=27): # leave on q or ESC
-        if ch==curses.KEY_UP or ch==107 or ch==16:
-            if highlight!=0:
-                w.chgat(highlight,0, 0)
-                highlight -= 1
-                while(items[highlight][0]==""):
-                    highlight -=1
-                w.chgat(highlight,0, curses.A_REVERSE)
-        if ch==curses.KEY_DOWN or ch==106 or ch==14:
-            if highlight!=len(items)-1:
-                w.chgat(highlight,0, 0)
-                highlight += 1
-                while(items[highlight][0]==""):
-                    highlight +=1
-                w.chgat(highlight,0, curses.A_REVERSE)
-        if ch==curses.KEY_PPAGE:
-            w.chgat(highlight,0, 0)
-            highlight = 0
-            w.chgat(highlight,0, curses.A_REVERSE)
-        if ch==curses.KEY_NPAGE:
-            w.chgat(highlight,0, 0)
-            highlight = len(items)-1
-            w.chgat(highlight,0, curses.A_REVERSE)
-        if ch==114 or ch==10:
-            (s,f)=items[highlight]
-            f()
-            redrawMenu(w,items,highlight)
-        w.refresh()
-        ch = w.getch()
-
-def redrawMenu(w,items,highlight):
-    i=0
-    for (mitem,fun) in items:
-        w.addstr(i,0, mitem)
-        i +=1
-    w.chgat(highlight, 0, curses.A_REVERSE)
-    w.refresh()
-    hb.commands=menu_commands
-    hb.refresh()
-
-
-def addForm():
-    w=curses.newwin(1,1)
-    (my,mx)=stdscr.getmaxyx()
-    bf = form.BookForm(w,hb,width=mx-20)
-    (r,c)=w.getmaxyx()
-    w.mvwin((my-r)//2,(mx-c)//2)
-    bf.lookup_isbn=book_data.openLibrary_isbn
-    bf.lookup_lccn=book_data.openLibrary_lccn
-    bf.caption='Add a Book'
-    bf.blabel = 'Add'
-    book = bf.event_loop()
-    bf.clear()
-    if len(book)!=0:
-        db.addBook(book)
-
-def browseMenu():
-    w=curses.newwin(3,5)
-    b = browser.bookBrowser(w,hb)
-    (r,c) = w.getmaxyx()
-    (my,mx)=stdscr.getmaxyx()
-    w.mvwin((my-r)//2 -2, (mx-c)//2)
-    b.refreshBooks()
-    b.eventLoop()
-    b.clear()
-
-def trashMenu():
-    w=curses.newwin(3,5)
-    b = browser.trashBrowser(w,hb)
-    (r,c) = w.getmaxyx()
-    (my,mx)=stdscr.getmaxyx()
-    w.mvwin((my-r)//2 -2, (mx-c)//2)
-    b.refreshBooks()
-    b.eventLoop()
-    b.clear()
-
-def checkedout_menu():
-    w=curses.newwin(3,5)
-    b = browser.bookBrowser(w,hb)
-    (r,c) = w.getmaxyx()
-    (my,mx)=stdscr.getmaxyx()
-    w.mvwin((my-r)//2 -2, (mx-c)//2)
-    b.load_data(db.get_checkedout_books())
-    b.columnDefs = [("id",0,3),
-                    ("uwid",0,8),
-                    ("date",0,10),
-                    ("title",100,None)]
-    b.calcColWidths()
-    b.eventLoop()
-    b.clear()
-
-def onshelf_menu():
-    w=curses.newwin(3,5)
-    b = browser.bookBrowser(w,hb)
-    (r,c) = w.getmaxyx()
-    (my,mx)=stdscr.getmaxyx()
-    w.mvwin((my-r)//2 -2, (mx-c)//2)
-    b.load_data(db.get_onshelf_books())
-    b.eventLoop()
-    b.clear()
-
-def co_menu():
-    w=curses.newwin(1,1)
-    (my,mx)=stdscr.getmaxyx()
-    co.checkout_procedure(w,hb,my//2,mx//2,mx) 
-
-def return_menu():
-    w=curses.newwin(1,1)
-    (my,mx)=stdscr.getmaxyx()
-    co.return_procedure(w,hb,my//2,mx//2,mx) 
-
-def catMenu():
-    (my,mx)=stdscr.getmaxyx()
-    w=curses.newwin(3,5)
-    cat = browser.categoryBrowser(w,hb, 10,40)
-    (r,c) = w.getmaxyx()
-    w.mvwin((my-r)//2 -2, (mx-c)//2)
-    cat.refreshCategories()
-    cat.sortByColumn('category')
-    cat.eventLoop()
-    cat.clear()
-
-
-if __name__ == "__main__":
-    db.initializeDatabase()
-    m = [("Browse Library", browseMenu),
-         ("Add Book", addForm),
-         ("Categories", catMenu),
-         ("View Trash", trashMenu),
-         ("",exit),
-         ("Check Out a Book", co_menu),
-         ("Return a Book", return_menu),
-         ("",exit),
-         ("View Checked Out Books", checkedout_menu),
-         ("View On Shelf Books", onshelf_menu),
-         ("",exit),
-         ("Exit", exit)]
-    curses.wrapper(menutest, m)
-
-
diff --git a/library/__init__.py b/library/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/library/book_data.py b/library/book_data.py
new file mode 100644 (file)
index 0000000..6a4f4ca
--- /dev/null
@@ -0,0 +1,137 @@
+try:
+        # For Python 3.0 and later
+            from urllib.request import urlopen,URLError
+except ImportError:
+        # Fall back to Python 2's urllib2
+            from urllib2 import urlopen,URLError
+from json import loads,dumps
+from socket import timeout
+
+""" Library Book Type Description:
+The book is a dictionary of the form { string : a, ... }
+
+Keys:
+  required: (ideally)
+    title - Book/Article title
+    publisher - string containing semi-colon separated list eg. "UW Press; CSC, inc."
+    authors - as above. each name is of the form "First Initials. Last" eg. "Calum T. Dalek; Conan T.B. Ladan"
+  optional:
+    subtitle - string
+    edition - integer
+    isbn - integer (it's preferred to use the isbn-13 rather than isbn-10)
+    lccn - integer: library of congress catalogue number
+    publish date - string of date (to make things easier to code/catalogue (won't be stored)
+    publish year - int (this kind of thing will have to be confirmed by cataloguer)
+    publish month - int
+    publish location - like publisher
+    
+    pages - integer - just the number of pages
+    pagination - string eg. "xviii, 1327-1850"
+    weight - string (purely for interest's sake eg. "3lb." or "3 pounds"
+    categories - list of strings?
+"""
+
+
+# look up data from openlibrary.org using isbn
+def openLibrary_isbn(ISBN):
+    isbn = str(ISBN)
+    try:
+        jsondata = urlopen("http://openlibrary.org/api/books"
+                           "?format=json&jscmd=data&bibkeys=ISBN:"+isbn,
+                           timeout=3)
+    except URLError as e:
+        return {'title':e}
+    except timeout:
+        return {'title':'Timeout while connecting to OpenLibrary.org'}
+    openBook = loads(jsondata.read().decode('utf-8'))
+    if "ISBN:"+isbn not in openBook:
+        return {'isbn':isbn,'title':'Book not found'}
+    openBook = openBook["ISBN:"+isbn]
+    # create my custom dict for books with the info we want.
+    book = dict({"isbn" : isbn})
+    book["title"]=openBook["title"]
+    book["authors"]=""
+    if "authors" in openBook:
+        for v in openBook["authors"]:
+            book['authors'] += "; " + v['name']
+        book['authors'] = book['authors'][2:]
+    book["publisher"]=""
+    if "publishers" in openBook:
+        for v in openBook["publishers"]:
+            book["publisher"] += "; " + v['name']
+        book['publisher'] = book['publisher'][2:]
+    if "publish_places" in openBook:
+        book["publish location"]=""
+        for v in openBook["publish_places"]:
+            book["publish location"] += "; " + v['name']
+        book['publish location'] = book['publish location'][2:]
+
+    # for lccn, there maybe be multiple values in the query. I'm just taking
+    # the first, but the full list may be useful
+    if "lccn" in openBook['identifiers']:
+        book["lccn"]=int(openBook['identifiers']['lccn'][0])
+    if "publish_date" in openBook:
+        book['publish date']=openBook['publish_date']
+        #code to pull out year and month (hopefully)
+    if "number_of_pages" in openBook:
+        book["pages"]=openBook["number_of_pages"]
+    if "pagination" in openBook:
+        book["pagination"]=openBook["pagination"]
+    if "weight" in openBook:
+        book["weight"]=openBook["weight"]
+    if "subtitle" in openBook:
+        book["subtitle"]=openBook["subtitle"]
+    return book
+
+# look up data from openlibrary.org using lccn
+def openLibrary_lccn(LCCN):
+    lccn = str(LCCN)
+    try:
+        jsondata = urlopen("http://openlibrary.org/api/books"
+                           "?format=json&jscmd=data&bibkeys=lccn:"+lccn,
+                           timeout=3)
+    except URLError:
+        return {}
+    openBook = loads(jsondata.read().decode('utf-8'))
+    if "lccn:"+lccn not in openBook:
+        return {'lccn':lccn,'title':'Book not found'}
+    openBook = openBook["lccn:"+lccn]
+    # create my custom dict for books with the info we want.
+    book = {"lccn" : lccn}
+    book["title"]=openBook["title"]
+    book["authors"]=""
+    if "authors" in openBook:
+        for v in openBook["authors"]:
+            book['authors'] += "; " + v['name']
+        book['authors'] = book['authors'][2:]
+    book["publisher"]=""
+    if "publishers" in openBook:
+        for v in openBook["publishers"]:
+            book["publisher"] += "; " + v['name']
+        book['publisher'] = book['publisher'][2:]
+    if "publish_places" in openBook:
+        book["publish location"]=""
+        for v in openBook["publish_places"]:
+            book["publish location"] += "; " + v['name']
+        book['publish location'] = book['publish location'][2:]
+
+    # for isbn, there maybe be multiple values in the query. I'm just taking
+    # the first, but the full list may be useful
+    # There are also ISBN's that have non-number values :(
+    if "isbn_10" in openBook['identifiers']:
+        book["isbn"]=openBook['identifiers']['isbn_10'][0]
+    if "isbn_13" in openBook['identifiers']:
+        book["isbn"]=openBook['identifiers']['isbn_13'][0]
+    if "publish_date" in openBook:
+        book['publish date']=openBook['publish_date']
+        #code to pull out year and month (hopefully)
+    if "number_of_pages" in openBook:
+        book["pages"]=openBook["number_of_pages"]
+    if "pagination" in openBook:
+        book["pagination"]=openBook["pagination"]
+    if "weight" in openBook:
+        book["weight"]=openBook["weight"]
+    if "subtitle" in openBook:
+        book["subtitle"]=openBook["subtitle"]
+    return book
+
diff --git a/library/database.py b/library/database.py
new file mode 100644 (file)
index 0000000..45ad4e6
--- /dev/null
@@ -0,0 +1,379 @@
+import sqlite3
+
+from  library import permissions
+
+_catalogue_db_file = '/users/libcom/catalogue.db'
+_book_table = 'books'
+_book_category_table='book_categories'
+_category_table = 'categories'
+
+_checkout_db_file = '/users/libcom/checkout.db'
+_checkout_table = 'checked_out'
+_return_table = 'returned'
+
+_checkout_table_creation = '''
+CREATE TABLE IF NOT EXISTS checked_out
+    (id INTEGER UNIQUE, uwid STRING, date_out DATETIME DEFAULT current_timestamp);
+
+CREATE TABLE IF NOT EXISTS returned
+    (id INTEGER, uwid STRING, date_out DATETIME, date_in DATETIME DEFAULT current_timestamp);
+'''
+
+_book_table_creation = '''
+CREATE TABLE IF NOT EXISTS books
+    (id INTEGER PRIMARY KEY AUTOINCREMENT, 
+     isbn, lccn, title, subtitle, authors, edition, 
+     publisher, publish_year, publish_month, publish_location, 
+     pages, pagination, weight,
+     last_updated DATETIME DEFAULT current_timestamp,
+     deleted BOOLEAN DEFAULT 0);
+
+CREATE TABLE IF NOT EXISTS categories
+    (cat_id INTEGER PRIMARY KEY, category STRING UNIQUE ON CONFLICT IGNORE);
+
+CREATE TABLE IF NOT EXISTS book_categories
+    (id INTEGER, cat_id INTEGER);
+'''
+
+columns = ['id', 'isbn', 'lccn',
+           'title', 'subtitle', 'authors', 'edition', 
+           'publisher', 'publish year', 'publish month', 'publish location', 
+           'pages', 'pagination', 'weight', 'last updated', 'deleted']
+
+_book_trigger_creation = '''
+
+CREATE TRIGGER IF NOT EXISTS update_books_time AFTER UPDATE ON books
+BEGIN
+    UPDATE books SET last_updated = DATETIME('NOW') WHERE rowid = new.rowid;
+END;
+
+CREATE TRIGGER IF NOT EXISTS delete_book AFTER DELETE ON books
+BEGIN
+    DELETE FROM book_categories WHERE id = old.rowid;
+END;
+
+CREATE TRIGGER IF NOT EXISTS delete_category AFTER DELETE ON categories
+BEGIN
+    DELETE FROM book_categories WHERE cat_id = old.cat_id;
+END;
+
+CREATE TRIGGER IF NOT EXISTS insert_book_category_time AFTER INSERT
+ON book_categories
+BEGIN
+    UPDATE books SET last_updated = DATETIME('NOW') WHERE id = new.id;
+END;
+'''
+
+#################################
+# character escaping, etc for sql queries
+#################################
+def _colify(s):
+    return s.replace(" ","_").lower()
+
+def _stringify(v):
+    return '"' + str(v).strip().replace('"','""') + '"'
+
+###################################
+# book functions
+##################################
+@permissions.check_permissions(permissions.PERMISSION_LIBCOM)
+def addBook(book):
+    conn = sqlite3.connect(_catalogue_db_file)
+    c = conn.cursor()
+    cols = []
+    vals = []
+    for k,v in book.items():
+        if v!="":
+            cols.append(_colify(k))
+            vals.append(_stringify(v))
+    
+    query = ("INSERT INTO "+_book_table+" ("+", ".join(cols)+") VALUES ("+
+             ", ".join(vals)+");")
+    c.execute(query)
+    conn.commit()
+    c.close()
+
+@permissions.check_permissions(permissions.PERMISSION_LIBCOM)
+def updateBook(book, bookID):
+    '''
+    Takes book attribute dictionary and a string representating the book ID
+    number, and returns updates the book accordingly
+    '''
+    conn = sqlite3.connect(_catalogue_db_file)
+    c = conn.cursor()
+    updates=[]
+    for k,v in book.items():
+        updates.append(_colify(k)+"="+_stringify(v))
+    query = ("UPDATE "+_book_table+" SET " +  ", ".join(updates)+" WHERE id = " +
+             str(bookID)+";")
+    c.execute(query)
+    conn.commit()
+    c.close()
+
+def get_books():
+    conn = sqlite3.connect(_catalogue_db_file)
+    c = conn.cursor()
+    query = "SELECT * FROM "+_book_table+" WHERE deleted=0;"
+    c.execute(query)
+    books = [_query_to_book(b) for b in c]
+    c.close()
+    return books
+
+def getBooksByCategory(cat):
+    '''
+    Takes a string representating the category ID number, and returns
+    non-deleted books in that category
+    '''
+    conn = sqlite3.connect(_catalogue_db_file)
+    c = conn.cursor()
+    query = ("SELECT "+",".join(map(_colify,columns))+" FROM "+_book_table+
+             " JOIN "+_book_category_table+
+             " USING (id) WHERE cat_id = :id AND deleted=0;")
+    c.execute(query,cat)
+    books = [_query_to_book(b) for b in c]
+    c.close()
+    return books
+
+def getRemovedBooks():
+    conn = sqlite3.connect(_catalogue_db_file)
+    c = conn.cursor()
+    query = "SELECT * FROM "+_book_table+" WHERE DELETED=1;"
+    c.execute(query)
+    books = [_query_to_book(b) for b in c]
+    c.close()
+    return books
+
+def get_book(bookid):
+    conn = sqlite3.connect(_catalogue_db_file)
+    c = conn.cursor()
+    query = "SELECT * FROM "+_book_table+" WHERE id = "+str(bookid)+";"
+    c.execute(query)
+    book = _query_to_book(c.fetchone())
+    c.close()
+    return book
+
+# removes book from catalogue
+@permissions.check_permissions(permissions.PERMISSION_LIBCOM)
+def removeBook(bookid):
+    conn = sqlite3.connect(_catalogue_db_file)
+    c = conn.cursor()
+    query = "UPDATE " +_book_table+ " SET deleted=1 WHERE id = "+str(bookid)+";"
+    c.execute(query)
+    conn.commit()
+    c.close()
+
+@permissions.check_permissions(permissions.PERMISSION_LIBCOM)
+def removeBooks(books):
+    conn = sqlite3.connect(_catalogue_db_file)
+    c = conn.cursor()
+    query1 = "UPDATE " +_book_table+ " SET deleted=1 WHERE id = :id;"
+    for book in books:
+        c.execute(query1, book)
+    conn.commit()
+    c.close()
+
+# restores trashed books
+@permissions.check_permissions(permissions.PERMISSION_LIBCOM)
+def restoreBooks(books):
+    conn = sqlite3.connect(_catalogue_db_file)
+    c = conn.cursor()
+    query1 = "UPDATE " +_book_table+ " SET deleted=0 WHERE id = :id;"
+    for book in books:
+        c.execute(query1,book)
+    conn.commit()
+    c.close()
+
+# fully deletes book from books table
+@permissions.check_permissions(permissions.PERMISSION_LIBCOM)
+def deleteBook(bookid):
+    conn = sqlite3.connect(_catalogue_db_file)
+    c = conn.cursor()
+    query = "DELETE FROM " +_book_table+ " WHERE id = "+str(bookid)+";"
+    c.execute(query)
+    conn.commit()
+    c.close()
+
+@permissions.check_permissions(permissions.PERMISSION_LIBCOM)
+def deleteBooks(books):
+    conn = sqlite3.connect(_catalogue_db_file)
+    c = conn.cursor()
+    query = "DELETE FROM " +_book_table+ " WHERE id = :id;"
+    for book in books:
+        c.execute(query, book)
+    conn.commit()
+    c.close()
+
+def _query_to_book(book_query):
+    # Make a dict out of column name and query results.
+    # Empty entries return None, which are removed from the dict.
+    return dict(filter(lambda t:t[1], zip(columns,book_query)))
+
+def _query_to_book_checkout(book_query):
+    # Make a dict out of column name and query results.
+    # Empty entries return None, which are removed from the dict.
+    b = _query_to_book(book_query)
+    b['uwid'] = book_query[-2]
+    b['date'] = book_query[-1]
+    return b
+
+#########################################
+# Category related functions
+########################################
+def getBookCategories(book):
+    conn = sqlite3.connect(_catalogue_db_file)
+    c = conn.cursor()
+    query = ("SELECT id,cat_id,category FROM "+_book_category_table+" JOIN "+
+             _category_table+" USING (cat_id) WHERE id = :id ;")
+    c.execute(query,book)
+    cats = []
+    for book_id,cat_id,cat_name in c:
+        cats.append({'id':book_id, 'cat_id':cat_id, 'category':cat_name})
+    c.close()
+    return cats
+
+@permissions.check_permissions(permissions.PERMISSION_LIBCOM)
+def categorizeBook(book, cats):
+    conn = sqlite3.connect(_catalogue_db_file)
+    c = conn.cursor()
+    query = ("INSERT OR IGNORE INTO "+_book_category_table+
+             " (id,cat_id) VALUES (?, ?);")
+    for cat in cats:
+        args = (book['id'],cat['id'])
+        c.execute(query,args)
+    conn.commit()
+    c.close()
+
+@permissions.check_permissions(permissions.PERMISSION_LIBCOM)
+def uncategorizeBook(book, cats):
+    conn = sqlite3.connect(_catalogue_db_file)
+    c = conn.cursor()
+    query = "DELETE FROM "+_book_category_table+" WHERE (id = ? AND cat_id = ?);"
+    for cat in cats:
+        args = (book['id'],cat['id'])
+        c.execute(query,args)
+    conn.commit()
+    c.close()
+
+def getCategories():
+    conn = sqlite3.connect(_catalogue_db_file)
+    c = conn.cursor()
+    query = "SELECT cat_id, category FROM "+_category_table+";"
+    c.execute(query)
+    cats = []
+    for cat_id,cat in c:
+        cats.append({'id':cat_id, 'category':cat})
+    c.close()
+    return cats
+
+@permissions.check_permissions(permissions.PERMISSION_LIBCOM)
+def addCategory(cat):
+    conn = sqlite3.connect(_catalogue_db_file)
+    c = conn.cursor()
+    query = ("INSERT OR IGNORE INTO "+_category_table+" (category) VALUES ("
+             +_stringify(cat)+");")
+    c.execute(query)
+    conn.commit()
+    c.close()
+
+@permissions.check_permissions(permissions.PERMISSION_LIBCOM)
+def deleteCategories(cats):
+    conn = sqlite3.connect(_catalogue_db_file)
+    c = conn.cursor()
+    query1 = "DELETE FROM " +_category_table+ " WHERE cat_id = :id;"
+    for cat in cats:
+        c.execute(query1, cat)
+    conn.commit()
+    c.close()
+
+#########################################
+# Book Checkout functions
+#########################################
+@permissions.check_permissions(permissions.PERMISSION_OFFICE)
+def checkout_book(book_id, uwid):
+    conn = sqlite3.connect(_checkout_db_file)
+    c = conn.cursor()
+    query = "INSERT INTO " + _checkout_table + " (id, uwid) VALUES (?, ?);"
+    c.execute(query, (book_id, uwid))
+    conn.commit()
+    c.close()
+
+@permissions.check_permissions(permissions.PERMISSION_OFFICE)
+def return_book(book_id):
+    conn = sqlite3.connect(_checkout_db_file)
+    c = conn.cursor()
+    query = "SELECT uwid,date_out FROM "+ _checkout_table + " WHERE id = :id ;"
+    c.execute(query, {"id": book_id})
+    tmp = c.fetchone()
+    uwid = tmp[0]
+    date_out = tmp[1]
+    query = "INSERT INTO " + _return_table + " (id, uwid, date_out) VALUES (?, ?, ?);"
+    query2 = "DELETE FROM " + _checkout_table + " WHERE id= :id ;"
+    c.execute(query, (book_id, uwid, date_out))
+    c.execute(query2, {"id": book_id});
+    conn.commit()
+    c.close()
+
+def get_checkedout_books():
+    '''
+    retrieves checked out books. The returned books also have the fields
+    uwid: ID of person who signed out the book, and
+    date: date when the book was checked out
+    '''
+    conn = sqlite3.connect(_catalogue_db_file)
+    c = conn.cursor()
+    query = 'ATTACH "' + _checkout_db_file + '" AS co'
+    c.execute(query)
+    query = ("SELECT "+",".join(map(_colify,columns))+",uwid,date_out FROM "+_book_table+
+             " JOIN co."+_checkout_table+
+             " USING (id) ;")
+    c.execute(query)
+    books = [_query_to_book_checkout(b) for b in c]
+    c.close()
+    return books
+
+def get_onshelf_books():
+    '''
+    retrieves checked out books. The returned books also have the fields
+    uwid: ID of person who signed out the book, and
+    date: date when the book was checked out
+    '''
+    conn = sqlite3.connect(_catalogue_db_file)
+    c = conn.cursor()
+    query = 'ATTACH "' + _checkout_db_file + '" AS co'
+    c.execute(query)
+    query = ("SELECT "+",".join(map(_colify,columns))+" FROM "+_book_table+
+             " LEFT JOIN co."+_checkout_table+
+             " USING (id) WHERE uwid ISNULL;")
+    c.execute(query)
+    books = [_query_to_book(b) for b in c]
+    c.close()
+    return books
+
+#########################################
+# Database initialization
+#########################################
+def _createBooksTable():
+    conn = sqlite3.connect(_catalogue_db_file)
+    c = conn.cursor()
+    c.executescript(_book_table_creation)
+    conn.commit()
+    c.close()
+
+def _createTriggers():
+    conn = sqlite3.connect(_catalogue_db_file)
+    c = conn.cursor()
+    c.executescript(_book_trigger_creation)
+    conn.commit()
+    c.close()
+
+def _create_checkout_table():
+    conn = sqlite3.connect(_checkout_db_file)
+    c = conn.cursor()
+    c.executescript(_checkout_table_creation)
+    conn.commit()
+    c.close()
+
+def initializeDatabase():
+    _createBooksTable()
+    _createTriggers()
+    _create_checkout_table()
diff --git a/library/exceptions.py b/library/exceptions.py
new file mode 100644 (file)
index 0000000..a82df8a
--- /dev/null
@@ -0,0 +1,17 @@
+import abc
+
+class LibrarianException(Exception, metaclass=abc.ABCMeta):
+    @abc.abstractproperty
+    def error_msg(self):
+        pass
+
+    def __str__(self):
+        return self.error_msg
+
+class PermissionsError(LibrarianException):
+    def __init__(self, permission_string):
+        self.permission_string = permission_string
+
+    @property
+    def error_msg(self):
+        return "Need privilege level {}".format(self.permission_string)
diff --git a/library/interface/__init__.py b/library/interface/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/library/interface/browser.py b/library/interface/browser.py
new file mode 100644 (file)
index 0000000..b460b96
--- /dev/null
@@ -0,0 +1,558 @@
+import curses
+import library.database as db
+from library.interface.form import BookForm,CategoryForm
+
+class browserWindow:
+    # These are actually class variables, not member variables? :<
+    _default_height=25
+    _default_width=60
+
+    hl=0
+    topline = 0
+    entries = []
+    selected = list()
+    commands = [(' /', 'search'), (' n', 'find next'), (' N', 'find previous'),
+                ('F6', 'Sort Column'), (' q', 'quit')]
+    cs = []
+    # column definitions are in (label, weight, specified width) triples
+    columnDefs = [('something',1,None)]
+    mx = my = 0
+    # for searches
+    last_search = ""
+    found_index = 0
+
+    def __init__(self,window,helpbar, height=0, width=0):
+        if not(height and width):
+            height = browserWindow._default_height
+            width = browserWindow._default_width
+        self.w = window
+        self.hb = helpbar
+        self.w.resize(height,width)
+        self.updateGeometry()
+        self.commands = self.cs+self.commands
+
+    def load_data(self, data=[]):
+        self.entries = data
+        self.selected = list(map(lambda x:False, self.entries))
+
+    def sortByColumn(self, col):
+        self.entries.sort(key=lambda k: k.get(col,"")) # key=dict.get(col))
+        self.selected = list(map(lambda x: False, self.selected))
+
+
+    def updateGeometry(self):
+        (self.my,self.mx)=self.w.getmaxyx()
+        self.pageSize = self.my-4
+        self.calcColWidths()
+
+    def calcColWidths(self):
+        total_weights = 0
+        available_space = self.mx - len(self.columnDefs) -2
+        cols = []
+        for label,weight,value in self.columnDefs:
+            if value!=None:
+                available_space -= value
+            else:
+                total_weights+=weight
+
+        for label,weight,value in self.columnDefs:
+            if value!=None:
+                cols.append((label,value))
+            else:
+                cols.append((label,available_space*weight//total_weights))
+        self.columns=cols
+
+    def refresh(self):
+        self.hb.commands = self.commands
+        self.hb.refresh()
+        self.w.box()
+        self.displayHeader()
+        for r in range(0,self.pageSize):
+            self.displayRow(r)
+        self.w.refresh()
+        self.highlight()
+
+    def clear(self):
+        self.w.erase()
+        self.w.refresh()
+
+    def centreChild(self,child):
+        (y,x) = self.w.getbegyx()
+        (r,c) = child.getmaxyx()
+        child.mvwin( y+(self.my-r)//2,x+(self.mx-c)//2)
+
+
+    def displayHeader(self):
+        cursor = 2
+        for header,width in self.columns:
+            self.w.addnstr(1,cursor,header+" "*width,width)
+            self.w.addstr(2,cursor,"-"*width)
+            cursor += width+1
+
+    def displayRow(self,row):
+        if self.topline+row < len(self.entries):
+            entry = self.entries[self.topline+row]
+            cursor = 2
+            self.w.addnstr(row+3, 1, " "*self.mx,self.mx-2)
+            if self.selected[self.topline+row]:
+                self.w.addstr(row+3, 1, "*")
+            else:
+                self.w.addstr(row+3, 1, " ")
+            for k,width in self.columns:
+                if k.lower() in entry:
+                    self.w.addnstr(row+3, cursor,
+                                   str(entry[k.lower()])+" "*width, width)
+                cursor += width+1
+        else:
+            self.w.addstr(row+3,1," "*(self.mx-2))
+
+    def highlight(self):
+        row = self.hl-self.topline+3
+        if row > 1 and row < self.my:
+            self.w.chgat(row,1,self.mx-2,curses.A_REVERSE)
+
+    def unHighlight(self):
+        row = self.hl-self.topline+3
+        if row > 1 and row < self.my:
+            self.w.chgat(row,1,self.mx-2,curses.A_NORMAL)
+
+    def mvHighlight(self,delta):
+        new = self.hl+delta
+        new = max(new,0)
+        new = min(new,len(self.entries)-1)
+        self.unHighlight()
+        self.hl = new
+        self.highlight()
+    
+    def scroll(self,delta):
+        self.unHighlight()
+        self.topline += delta
+        self.topline = min(self.topline,len(self.entries)-1)
+        self.topline = max(self.topline,0)
+        self.refresh()
+
+    def search(self, string):
+        case_sensitive = not(string.islower())
+        i = 0
+        found = False
+        for e in self.entries:
+            for k,v in e.items():
+                # we or with found to make sure it is never "unfound"
+                if case_sensitive:
+                    found = str(v).find(string) != -1 or found
+                else:
+                    found = str(v).lower().find(string) != -1 or found
+            if found:
+                break
+            i += 1;
+        if found:
+            self.last_search = string
+            self.search_index = i
+            self.case_sensitive = case_sensitive
+            return i
+        else:
+            self.search_index = -1
+            return -1
+
+    def _find_again(self, direction=1):
+        """Find the next match in the entries
+
+        direction = 1 means look ahead
+        direction = -1 means look back
+        """
+        if self.last_search == "" or self.search_index == -1:
+            return -1
+        found = False
+        if direction == 1:
+            last = len(self.entries) -1
+        elif direction == -1:
+            last = 0
+        for i in range(self.hl+direction, last, direction):
+            for k,v in self.entries[i].items():
+                if self.case_sensitive:
+                    found = str(v).find(self.last_search) != -1 or found
+                else:
+                    found = str(v).lower().find(self.last_search) != -1 or found
+            if found:
+                break
+        if found:
+            self.search_index = i
+            return i
+        else:
+            return -1
+
+    def eventLoop(self):
+        self.w.keypad(1)
+        self.refresh()
+
+        ch = self.w.getch()
+        while ch != 27 and ch != 113:
+            ch = self.handleInput(ch)
+            if ch==113:
+                return {}
+            self.w.refresh()
+            ch = self.w.getch()
+            self.hb.refresh()
+
+    def handleInput(self,ch):
+        if ch == curses.KEY_UP or ch == 107 or ch == 16:
+            if self.hl == self.topline:
+                self.scroll(-self.pageSize//2-1)
+            self.mvHighlight(-1)
+        elif ch == curses.KEY_DOWN or ch == 106 or ch == 14:
+            if self.hl == self.topline+self.pageSize-1:
+                self.scroll(+self.pageSize//2+1)
+            self.mvHighlight(+1)
+        elif ch == curses.KEY_PPAGE:
+            self.scroll(-self.pageSize)
+            self.mvHighlight(-self.pageSize)
+        elif ch == curses.KEY_NPAGE:
+            self.scroll(+self.pageSize)
+            self.mvHighlight(+self.pageSize)
+        elif ch == curses.KEY_HOME:
+            self.scroll(-len(self.entries))
+            self.mvHighlight(-len(self.entries))
+        elif ch == curses.KEY_END:
+            self.scroll(len(self.entries))
+            self.mvHighlight(len(self.entries))
+        elif ch == 47: # forward slash
+            string = self.hb.getSearch()
+            hl = self.search(string)
+            if hl != -1:
+                delta = hl - self.hl
+                self.scroll(delta)
+                self.mvHighlight(delta)
+            else:
+                self.hb.display(string+' not found')
+        elif ch == 110: # n
+            hl = self._find_again(+1)
+            if hl != -1:
+                delta = hl - self.hl
+                self.scroll(delta)
+                self.mvHighlight(delta)
+            else:
+                self.hb.display(self.last_search+' not found')
+        elif ch == 78: # N
+            hl = self._find_again(-1)
+            if hl != -1:
+                delta = hl - self.hl
+                self.scroll(delta)
+                self.mvHighlight(delta)
+            else:
+                self.hb.display(self.last_search+' not found')
+        elif ch == 270: # F6 Sorts
+            w = curses.newwin(1,1)
+            cl = columnSelector(w,self.hb,40,20)
+            self.centreChild(w)
+            col = cl.eventLoop()
+            cl.clear()
+            self.sortByColumn(col)
+            self.clear()
+            self.refresh()
+        elif ch == 32:
+            if len(self.selected)>0:
+                self.selected[self.hl] = not self.selected[self.hl]
+            self.displayRow(self.hl-self.topline)
+            self.highlight()
+
+
+
+class trashBrowser(browserWindow):
+    columnDefs = [('ID',0,3),
+                  ('ISBN',0,13),
+                  ('Authors',30,None),
+                  ('Title',60,None)]
+    
+    cs = [(' r', 'restore selected'), (' d', 'delete selected')]
+
+    
+    # redefinable functions
+    def viewSelection(self,book):
+        bookid = book['id']
+        w=curses.newwin(1,1)
+        bf = BookForm(w, self.hb, book, width=self.mx-10)
+        self.centreChild(w)
+        bf.caption='Viewing Book '+str(bookid)
+        bf.blabel='done'
+        bf.event_loop()
+        bf.clear()
+
+    def restoreSelected(self):
+        books = []
+        for sel,book in zip(self.selected, self.entries):
+            if sel:
+                books.append(book)
+        db.restoreBooks(books)
+
+    def delSelected(self):
+        books = []
+        for sel,book in zip(self.selected, self.entries):
+            if sel:
+                books.append(book)
+        db.deleteBooks(books)
+
+    def refreshBooks(self):
+        self.load_data(db.getRemovedBooks())
+
+    def handleInput(self,ch):
+        browserWindow.handleInput(self,ch)
+        if ch == 10:
+            book = self.entries[self.hl]
+            self.viewSelection(book)
+            self.refresh()
+        if ch==114: #restore books
+            count=0
+            for s in self.selected[0:self.hl-1]:
+                if s:
+                    count+=1
+            self.restoreSelected()
+            self.refreshBooks()
+            self.refresh()
+            self.scroll(-count)
+            self.mvHighlight(-count)
+        if ch==100: # delete books
+            count=0
+            for s in self.selected[0:self.hl-1]:
+                if s:
+                    count+=1
+            self.delSelected()
+            self.refreshBooks()
+            self.refresh()
+            self.scroll(-count)
+            self.mvHighlight(-count)
+        return ch
+
+class bookBrowser(browserWindow):
+    columnDefs = [('ID',0,3),
+                  ('ISBN',0,13),
+                  ('Authors',30,None),
+                  ('Title',60,None)]
+    
+    cs = [(' u', 'update'), (' d', 'delete selected')]
+
+    
+    # redefinable functions
+    def updateSelection(self,book):
+        bookid = book['id']
+        
+        w=curses.newwin(1,1)
+        bf = BookForm(w,self.hb,book, width=self.mx-20)
+        self.centreChild(w)
+        bf.caption='Update Book '+str(bookid)
+        bf.blabel='update'
+        newbook = bf.event_loop()
+        if len(newbook)!=0:
+            db.updateBook(newbook,bookid)
+        bf.clear()
+
+    def viewSelection(self,book):
+        bookid = book['id']
+        w=curses.newwin(1,1)
+        bf = BookForm(w,self.hb,book, width=self.mx-20)
+        self.centreChild(w)
+        bf.caption='Viewing Book '+str(bookid)
+        bf.blabel='done'
+        bf.event_loop()
+        bf.clear()
+
+    def categorizeSelection(self,book):
+        w = curses.newwin(1,1)
+        cs = categorySelector(w,self.hb,40,40)
+        self.centreChild(w)
+        cs.book = book
+        cs.refreshCategories()
+        cs.eventLoop()
+        cs.clear()
+    
+    def delSelected(self):
+        books = []
+        for sel,book in zip(self.selected, self.entries):
+            if sel:
+                books.append(book)
+        db.removeBooks(books)
+
+    def refreshBooks(self):
+        self.load_data(db.get_books())
+
+    def refreshBooksInCategory(self,cat):
+        self.load_data(db.getBooksByCategory(cat))
+
+    def handleInput(self,ch):
+        browserWindow.handleInput(self,ch)
+        if ch == 117: #update on 'u'
+            book = self.entries[self.hl]
+            self.updateSelection(book)
+            self.entries[self.hl]=db.get_book(book['id'])
+            self.refresh()
+        elif ch == 10:
+            book = self.entries[self.hl]
+            self.viewSelection(book)
+            self.refresh()
+        elif ch == 99:
+            book = self.entries[self.hl]
+            self.categorizeSelection(book)
+            self.refresh()
+        if ch==100:
+            count=0
+            for s in self.selected[0:self.hl-1]:
+                if s:
+                    count+=1
+            self.delSelected()
+            self.refreshBooks()
+            self.refresh()
+            self.scroll(-count)
+            self.mvHighlight(-count)
+        return ch
+
+class categoryBrowser(browserWindow):
+    columnDefs = [('Category',100,None)]
+    cs = [(' a', 'add category'), (' d', 'delete selected')]
+
+    def refreshCategories(self):
+        self.load_data(db.getCategories())
+        self.sortByColumn('category')
+
+    def addCategory(self):
+        w = curses.newwin(1,1,10,10)
+        cf = CategoryForm(w,self.hb)
+        self.centreChild(w)
+        cat = cf.event_loop()
+        db.addCategory(cat)
+        cf.clear()
+
+    def viewCategory(self):
+        w = curses.newwin(3,5)
+        b = bookBrowser(w,self.hb)
+        self.centreChild(w)
+        b.refreshBooksInCategory(self.entries[self.hl])
+        b.eventLoop()
+        b.clear()
+
+    def delSelected(self):
+        categories = []
+        for sel,cat in zip(self.selected, self.entries):
+            if sel:
+                categories.append(cat)
+        db.deleteCategories(categories)
+
+    def handleInput(self,ch):
+        browserWindow.handleInput(self,ch)
+        if ch==97:
+            self.addCategory()
+            self.refreshCategories()
+            self.refresh()
+        if ch ==10:
+            self.viewCategory()
+            self.refresh()
+        if ch==100:
+            count=0
+            for s in self.selected[0:self.hl-1]:
+                if s:
+                    count+=1
+            self.delSelected()
+            self.refreshCategories()
+            self.refresh()
+            self.scroll(-count)
+            self.mvHighlight(-count)
+        return ch
+
+class categorySelector(browserWindow):
+    columnDefs = [('Category',100,None)]
+    cs = [(' a', 'add category'), (' c', 'commit')]
+    book = {'id':''}
+    original=[]
+
+
+    def refreshCategories(self):
+        self.entries = db.getCategories()
+        self.sortByColumn('category')
+        self.refreshSelected()
+
+    def refreshSelected(self):
+        self.original = list(map(lambda x:False, self.entries))
+        cats = db.getBookCategories(self.book)
+        cats.sort()
+        cats.sort(key=lambda k: k.get('category')) # key=dict.get(col))
+        i = 0
+        j = 0
+        for cat in self.entries:
+            if i == len(cats):
+                break
+            if cat['id']==cats[i]['cat_id']:
+                self.original[j] = True;
+                i+=1
+            j+=1
+        self.selected = self.original[:]
+
+
+    def addCategory(self):
+        w = curses.newwin(1,1,10,10)
+        cf = CategoryForm(w,self.hb)
+        self.centreChild(w)
+        cats = cf.event_loop()
+        for c in cats:
+            db.addCategory(c)
+        cf.clear()
+
+    def updateCategories(self):
+        # first removed the deselected ones
+        uncats = []
+        cats = []
+        for old, new, category in zip(self.original, self.selected,
+                                      self.entries):
+            if old and not new:
+                uncats.append(category)
+            if not old and new:
+                cats.append(category)
+        db.uncategorizeBook(self.book, uncats)
+        # add the newly selected categories
+        db.categorizeBook(self.book, cats)
+
+
+    def handleInput(self,ch):
+        browserWindow.handleInput(self,ch)
+        if ch==97:
+            self.addCategory()
+            self.refreshCategories()
+            self.refresh()
+        if ch==99:
+            self.updateCategories()
+            return 113
+
+
+
+class columnSelector(browserWindow):
+    columnDefs = [('Column',100,None)]
+    entries = [
+            {'column': 'id'}, {'column': 'isbn'}, {'column': 'lccn'},
+            {'column': 'title'}, {'column': 'subtitle'}, {'column': 'authors'},
+            {'column': 'edition'}, {'column': 'publisher'}, 
+            {'column': 'publish year'}, {'column': 'publish month'}, 
+            {'column': 'publish location'}, {'column': 'pages'},
+            {'column': 'pagination'}, {'column': 'weight'},
+            {'column': 'last updated'},
+    ]
+
+    def __init__(self,window,helpbar,height=40,width=20):
+        self.selected = [False,False,False,False,False,False,False,
+                         False,False,False,False,False,False,False,False]
+        browserWindow.__init__(self,window,helpbar,height,width)
+
+
+    def eventLoop(self):
+        self.w.keypad(1)
+        self.refresh()
+
+        ch = self.w.getch()
+        while ch != 27 and ch != 113:
+            ch = self.handleInput(ch)
+            if ch==10:
+                col = self.entries[self.hl]
+                return col['column']
+            self.w.refresh()
+            ch = self.w.getch()
+            self.hb.refresh()
+    
+    def handleInput(self,ch):
+        browserWindow.handleInput(self,ch)
+        return ch
diff --git a/library/interface/checkout.py b/library/interface/checkout.py
new file mode 100644 (file)
index 0000000..59ef6a4
--- /dev/null
@@ -0,0 +1,98 @@
+import curses
+from library.interface.form import FormWindow,BookForm
+import library.database as db
+
+
+class BookIDForm(FormWindow):
+    caption = "Enter the book ID"
+    blabel = "Check"
+    labels = ["Book ID"]
+
+    def _return_values(self):
+        if self._confirm_book():
+            return self.entries[0].value
+        else:
+            return False
+
+    def _confirm_book(self):
+        self.clear()
+        bookid = self.entries[0].value
+        book = db.get_book(bookid)
+        bf = BookForm(self.w, self.hb, book, width=self.mx-10)
+        (y,x) = self.w.getbegyx()
+        (r,c) = self.w.getmaxyx()
+        self.w.mvwin( y+(self.my-r)//2,x+(self.mx-c)//2)
+        bf.caption='Confirm the Book '+str(bookid)
+        bf.blabel='Correct'
+        result = bf.event_loop()
+        bf.clear()
+        if result:
+            return True
+
+
+class UWIDForm(FormWindow):
+    caption = "Enter the Patron's username"
+    blabel = "Next"
+    labels = ["Username"]
+
+    def _return_values(self):
+        return self.entries[0].value
+
+class FinalCheck(FormWindow):
+    caption = "Is this information correct?"
+    blabel = "Check Out"
+    labels = ["Username", "Book ID"]
+
+    def _return_values(self):
+        return True
+
+def checkout_procedure(w, hb, cy, cx, mx):
+    """Procedure to check out a book
+
+    w:      ncurses window for the routine
+    cy,cx:  centre coordinates of the screen
+    mx:     max width of screen
+    """
+    # Get the book ID
+    step1 = BookIDForm(w,hb,width=mx-20)
+    (r,c)=w.getmaxyx()
+    w.mvwin(cy-r//2,cx-c//2)
+    book_id = step1.event_loop()
+    step1.clear()
+    if not(book_id):
+        return
+
+    # Get the uwid
+    step2 = UWIDForm(w,hb,width=mx-20)
+    (r,c)=w.getmaxyx()
+    w.mvwin(cy-r//2,cx-c//2)
+    username = step2.event_loop()
+    step2.clear()
+    if not(username):
+        return
+
+    # Confirm the result
+    step3 = FinalCheck(w,hb,book={"username":username,"book id":book_id}, width=mx-20)
+    (r,c)=w.getmaxyx()
+    w.mvwin(cy-r//2,cx-c//2)
+    correct = step3.event_loop()
+    step3.clear()
+    if correct:
+        db.checkout_book(book_id,username)
+
+def return_procedure(w, hb, cy, cx, mx):
+    """Procedure to return a book
+
+    w:      ncurses window for the routine
+    cy,cx:  centre coordinates of the screen
+    mx:     max width of screen
+    """
+    # Get the book ID
+    step1 = BookIDForm(w,hb,width=mx-20)
+    (r,c)=w.getmaxyx()
+    w.mvwin(cy-r//2,cx-c//2)
+    book_id = step1.event_loop()
+    step1.clear()
+    if book_id:
+        db.return_book(book_id)
+    
diff --git a/library/interface/form.py b/library/interface/form.py
new file mode 100644 (file)
index 0000000..839a41b
--- /dev/null
@@ -0,0 +1,318 @@
+import curses
+
+
+class TextEntry:
+
+    """A part of a window that handles text entry.
+    Properties:
+        value holds the string that was entered
+    
+    Public Methods:
+        set_geom(row,column,width)  Sets the geometry in the window
+        set_value(string)           Set the value and redraw
+        gain_focus()                Gives it focus, moving cursor and changing the drawing
+        lose_focus()                Takes focus, moving cursor to start, changing drawing
+        handle_input(ch)            Pass this the ncurses key, and it manages input
+        redraw()                    Redraw the text entry (should never need to do this
+        """
+
+    # Public
+    value = ""  # Use the set_value function to set, but retrieve with value
+
+    # Should be Private
+    cursor = 0
+    start = 0
+    focus = False
+    x = 0
+    y = 0
+    width = 10
+
+    # Public methods
+    def __init__(self, parent_window, value=""):
+        self.w = parent_window
+        self.value = value
+
+    def set_geom(self,y,x,width):
+        self.x = x
+        self.y = y
+        self.width = width
+
+    def set_value(self,v):
+        self.value=v
+        self.cursor=len(v)
+        self.redraw()
+
+    def gain_focus(self):
+        self.focus = True
+        self._mv_cursor(+len(self.value))
+        self.start = max(0,self.cursor-self.width) 
+        self.redraw()
+
+    def lose_focus(self):
+        self.focus = False
+        self.cursor = 0
+        self.start = 0
+        self.redraw()
+    
+    def handle_input(self,ch):
+        if ch==curses.KEY_LEFT:
+            self._mv_cursor(-1)
+        elif ch==curses.KEY_HOME:
+            self._set_cursor(0)
+        elif ch==curses.KEY_RIGHT:
+            self._mv_cursor(+1)
+        elif ch==curses.KEY_END:
+            self._set_cursor(len(self.value))
+        elif ch>=32 and ch<=126:
+            self._insert(curses.keyname(ch).decode('utf-8'))
+        elif ch==curses.KEY_BACKSPACE:
+            self._backspace()
+        elif ch==curses.KEY_DC:
+            self._delete()
+
+    def redraw(self):
+        self.w.addnstr(self.y,self.x, self.value[self.start:]+" "*self.width, self.width)
+        if self.focus:
+            self.w.chgat(self.y, self.x, self.width, curses.A_UNDERLINE)
+            curses.curs_set(1)
+
+    # Private functions
+    def _mv_cursor(self,delta):
+        self._set_cursor(self.cursor + delta)
+    
+    def _set_cursor(self, new_c):
+        self.cursor = max(0, min(len(self.value), new_c))
+        self.start = max(0,self.cursor-self.width+1) 
+        self.redraw()
+        # Place the drawn cursor in the correct spot
+        col = self.x + self.cursor - self.start
+        self.w.move(self.y,col)
+
+    def _insert(self,ch):
+        c = self.cursor
+        self.value = self.value[:c] +ch+  self.value[c:]
+        self._mv_cursor(+1)
+
+    def _backspace(self):
+        if self.cursor>0:
+            c = self.cursor
+            self.value=self.value[:c-1] + self.value[c:]
+            self._mv_cursor(-1)
+
+    def _delete(self):
+        c = self.cursor
+        self.value = self.value[:c] + self.value[c+1:]
+        self._mv_cursor(0)
+
+
+
+class FormWindow:
+
+    """General class for a Form Window.
+    
+    To use, make the window for it, call the constructor, then call event_loop.
+    """
+
+    # Private variables
+    mx = my = 0
+    hl = 0
+    bt = -1
+    left = 0
+    top = 2
+    row = 2
+    caption = "Form"
+    blabel = "Done"
+    labels = ["label1"]
+
+    commands = [('pU', 'top'),('pD', 'bottom'),('Es', 'cancel')]
+
+
+    # Public functions
+    def __init__(self,window,helpbar,book={}, width=50):
+        self.w = window
+        self.w.resize(len(self.labels)+6,width)
+        self.hb = helpbar
+        self._make_entries()
+        self._update_geometry()
+        self._set_entries(book)
+
+    def clear(self):
+        self.w.erase()
+        self.w.refresh()
+
+    def event_loop(self):
+        self.w.keypad(1)
+        self.refresh()
+        self.hl=0;
+        self.entries[self.hl].gain_focus()
+
+        ch = self.w.getch()
+        while ch != 27:
+            self.handle_input(ch)
+            if ch==10 or ch==curses.KEY_ENTER:
+                if self.bt==0:
+                    return {}
+                elif self.bt==1:
+                    return self._return_values()
+                else:
+                    self._mv_focus(+1)
+            self.w.refresh()
+            ch = self.w.getch()
+        curses.curs_set(0)
+        return {}
+
+    def _make_entries(self):
+        self.entries = []
+        for e in range(len(self.labels)):
+            self.entries.append(TextEntry(self.w))
+
+    def _update_geometry(self):
+        (self.my, self.mx) = self.w.getmaxyx()
+        self.left=0
+        for l in self.labels:
+            self.left = max(len(l),self.left)
+        self.left += 4
+        width = self.mx-self.left-2
+        self.top = 2
+        for r in range(len(self.entries)):
+            self.entries[r].set_geom(r+self.top, self.left, width)
+        # next, the buttons
+        self.brow = self.top+len(self.labels)+1
+        self.bcol = [self.mx-len(self.blabel)-14, self.mx-len(self.blabel)-4]
+        self.bwidth = [8,len(self.blabel)+2]
+
+    def _set_entries(self,book):
+        e = 0
+        for l in self.labels:
+            if l.lower() in book:
+                self.entries[e].value = str(book[l.lower()])
+            else:
+                self.entries[e].value = ""
+            e += 1 
+
+    def redraw(self):
+        self.w.box()
+        self.w.addstr(0,(self.mx-len(self.caption))//2,self.caption)
+        r=0
+        for l in self.labels:
+            c = self.left-len(l)-2
+            self.w.addstr(r+self.top,c,l+":")
+            self.entries[r].redraw()
+            r+=1
+        self.w.addstr(self.brow,self.bcol[0], "<cancel>  <"+self.blabel+">")
+        self.w.refresh()
+
+    def refresh(self):
+        self.hb.commands = self.commands
+        self.hb.refresh()
+        self._update_geometry()
+        self.redraw()
+
+    def _highlight_button(self):
+        self.w.chgat(self.brow, self.bcol[self.bt], self.bwidth[self.bt], curses.A_REVERSE)
+        curses.curs_set(0)
+
+    def _unhighlight_button(self):
+        self.w.chgat(self.brow,1,self.mx-2,curses.A_NORMAL)
+
+    def _mv_focus(self,delta):
+        if self.bt==-1:
+            self.entries[self.hl].lose_focus()
+        else:
+            self._unhighlight_button()
+        new = self.hl+delta
+        new = max(0, min(len(self.labels), new))   # the extra is for the buttons
+        self.hl = new
+        if new == len(self.labels):
+            self.bt = 1
+            self.bt = min(self.bt,1)
+            self._highlight_button()
+        else:
+            self.bt=-1
+            self.entries[self.hl].gain_focus()
+
+
+    def _return_values(self):
+        book = {}
+        for k,e in zip(self.labels, self.entries):
+            if e!="" and k.lower()!="publish date":
+                book[k.lower()]=e.value
+        return book
+
+
+    def handle_input(self,ch):
+        if ch==curses.KEY_UP:
+            self._mv_focus(-1)
+        elif ch==curses.KEY_PPAGE:
+            self._mv_focus(-len(self.labels))
+        elif ch==curses.KEY_DOWN:
+            self._mv_focus(+1)
+        elif ch==curses.KEY_NPAGE:
+            self._mv_focus(+len(self.labels))
+        elif ch==curses.KEY_LEFT:
+            if self.bt==-1:
+                self.entries[self.hl].handle_input(ch)
+            else:
+                self._unhighlight_button()
+                self.bt=0
+                self._highlight_button()
+        elif ch==curses.KEY_HOME:
+            if self.bt==-1:
+                self._mv_cursor(-len(self.entries[self.hl]))
+        elif ch==curses.KEY_RIGHT:
+            if self.bt==-1:
+                self.entries[self.hl].handle_input(ch)
+            else:
+                self._unhighlight_button()
+                self.bt=1
+                self._highlight_button()
+        else:
+            if self.bt==-1:
+                self.entries[self.hl].handle_input(ch)
+
+        
+        
+
+class BookForm(FormWindow):
+    caption = "Add a Book"
+    blabel = "Add"
+    labels = ["ISBN", "LCCN", "Title", "Subtitle", "Authors", "Edition",
+              "Publisher", "Publish Date", "Publish Year", "Publish Month", "Publish location",
+              "Pages", "Pagination", "Weight"]
+    
+
+    # redefineable functions lookup is called when 'enter' is pressed on ISBN
+    # and returns the looked-up book. Default returns nothing
+    def lookup_isbn(self,isbn):
+        return {'isbn':isbn}
+    
+    def lookup_lccn(self,lccn):
+        return {'lccn':lccn}
+
+    def return_book(self):
+        return self._return_values()
+
+    def handle_input(self,ch):
+        if ch==10 or ch==curses.KEY_ENTER:
+            if self.hl==0:          # lookup by isbn
+                book = self.lookup_isbn(self.entries[0].value)
+                if book != {}:
+                    self._set_entries(book)
+                self.refresh()
+                self._mv_focus(+7)
+            if self.hl==1:          # lookup by lccn
+                book = self.lookup_lccn(self.entries[1].value)
+                if book != {}:
+                    self._set_entries(book)
+                self.refresh()
+                self._mv_focus(+6)
+        else:
+            FormWindow.handle_input(self,ch)
+
+class CategoryForm(FormWindow):
+    caption = "Add a Category"
+    blabel = "Add"
+    labels = ["Category"]
+
+    def _return_values(self):
+        return self.entries[0].value
diff --git a/library/interface/help_bar.py b/library/interface/help_bar.py
new file mode 100644 (file)
index 0000000..880eeac
--- /dev/null
@@ -0,0 +1,70 @@
+import curses
+
+class helpBar:
+    # commands is in the form (key, command_name)
+    commands = []
+    mx=my=0
+    x=y=0
+    colWidth = 25
+    numCols=1
+
+    def __init__(self, window):
+        self.w = window
+
+    def updateGeometry(self):
+        (self.my, self.mx) = self.w.getmaxyx()
+        (self.y, self.x) = self.w.getbegyx()
+        self.numCols = self.mx//self.colWidth
+        numRows = len(self.commands)//self.numCols +1
+        self.y += self.my - numRows
+        self.my = numRows
+        self.w.mvwin(0,0)
+        self.w.resize(self.my,self.mx)
+        self.w.mvwin(self.y,self.x)
+
+    def refresh(self):
+        self.clear()
+        self.updateGeometry()
+        r=0
+        c=0
+        for key,command in self.commands:
+            self.w.addnstr(r,c,key+" "+command+" "*self.colWidth,
+                           self.colWidth-1)
+            self.w.chgat(r,c,2,curses.A_REVERSE)
+            c+=self.colWidth
+            if c > self.colWidth*self.numCols:
+                c=0
+                r+=1
+        self.w.refresh()
+
+    def clear(self):
+        self.w.erase()
+        self.w.refresh()
+
+    def getSearch(self):
+        self.clear()
+        self.w.addstr(0,0,"/")
+        string = ""
+        done = False
+        self.w.keypad(1)
+        ch = self.w.getch()
+        while (not done):
+            if ch == curses.KEY_ENTER or ch == 10:
+                return string
+            elif ch == 27: # escape
+                return ""
+            elif ch == curses.KEY_BACKSPACE and string !="":
+                self.w.addstr(0,1," "*len(string))
+                string = string[0:len(string)-1]
+                self.w.addstr(0,1,string)
+            elif ch>=32 and ch<=126:
+                char = curses.keyname(ch).decode('utf-8')
+                string = string + char
+                self.w.addstr(0,1,string)
+            self.w.refresh()
+            ch = self.w.getch()
+
+    def display(self,string):
+        self.clear()
+        self.w.addstr(0,1,string)
+        self.w.refresh()
diff --git a/library/permissions.py b/library/permissions.py
new file mode 100644 (file)
index 0000000..eb4003e
--- /dev/null
@@ -0,0 +1,34 @@
+import grp
+import os
+
+from library import exceptions
+
+class _PermissionLevel:
+    def __init__(self, group_name, pretty_name):
+        self.group_name = group_name
+        self.pretty_name = pretty_name
+
+PERMISSION_OFFICE = _PermissionLevel("office", "Office worker")
+PERMISSION_LIBCOM = _PermissionLevel("libcom", "Library Committee")
+
+def check_permissions(permission_level):
+    def decorator(fn):
+        def wrapped_function(*args, **kwargs):
+            if not has_permission(permission_level):
+                raise exceptions.PermissionsError(permission_level.pretty_name)
+            return fn(*args, **kwargs)
+
+        return wrapped_function
+
+    return decorator
+
+def has_permission(permission_level):
+    return permission_level.group_name in _CURRENT_GROUPS_GETTER()
+
+def _current_group_names():
+    group_ids = os.getgroups()
+    group_names = [grp.getgrgid(group_id).gr_name for group_id in group_ids]
+    return group_names
+
+# Hack to allow dependency injection for testing
+_CURRENT_GROUPS_GETTER = _current_group_names
diff --git a/permissions.py b/permissions.py
deleted file mode 100644 (file)
index 232a05c..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-import grp
-import os
-
-import exceptions
-
-class _PermissionLevel:
-    def __init__(self, group_name, pretty_name):
-        self.group_name = group_name
-        self.pretty_name = pretty_name
-
-PERMISSION_OFFICE = _PermissionLevel("office", "Office worker")
-PERMISSION_LIBCOM = _PermissionLevel("libcom", "Library Committee")
-
-def check_permissions(permission_level):
-    def decorator(fn):
-        def wrapped_function(*args, **kwargs):
-            if not has_permission(permission_level):
-                raise exceptions.PermissionsError(permission_level.pretty_name)
-            return fn(*args, **kwargs)
-
-        return wrapped_function
-
-    return decorator
-
-def has_permission(permission_level):
-    return permission_level.group_name in _CURRENT_GROUPS_GETTER()
-
-def _current_group_names():
-    group_ids = os.getgroups()
-    group_names = [grp.getgrgid(group_id).gr_name for group_id in group_ids]
-    return group_names
-
-# Hack to allow dependency injection for testing
-_CURRENT_GROUPS_GETTER = _current_group_names
diff --git a/setup.py b/setup.py
new file mode 100644 (file)
index 0000000..91a9e7e
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,8 @@
+from distutils.core import setup
+setup(name="librarian",
+      description="Library Management Software for CSC",
+      author="jladan"
+      version="1.0",
+      packages=['library','library.interface']
+      scripts=["librarian"]
+      )