sort books by any column
[public/library.git] / browser.py
1 import sys
2 import curses
3 import dbLayer as db
4 from form import bookForm,categoryForm
5
6 class browserWindow:
7     hl=0
8     topline = 0
9     entries = []
10     selected = []
11     commands = [(' /', 'search'), (' n', 'find next'), (' N', 'find previous'), (' q', 'quit')]
12     cs = []
13     # column definitions are in (label, weight, specified width) triples
14     columnDefs = [('something',1,None)]
15     mx = my = 0
16     cx = cy = 0
17     # for searches
18     last_search = ""
19     found_index = 0
20
21     def __init__(self,window,helpbar):
22         self.w = window
23         self.hb = helpbar
24         self.updateGeometry()
25         self.commands = self.cs+self.commands
26
27     def sortByColumn(self, col):
28         self.entries.sort(key=lambda k: k.get(col)) # key=dict.get(col))
29         self.selected = map(lambda x: False, self.selected)
30
31     def updateGeometry(self):
32         (self.my,self.mx)=self.w.getmaxyx()
33         (y,x) = self.w.getbegyx()
34         self.cx = x + self.mx/2
35         self.cy = y + self.my/2
36         self.pageSize = self.my-4
37         self.calcColWidths()
38
39     def calcColWidths(self):
40         total_weights = 0
41         available_space = self.mx - len(self.columnDefs) -2
42         cols = []
43         for label,weight,value in self.columnDefs:
44             if value!=None:
45                 available_space -= value
46             else:
47                 total_weights+=weight
48
49         for label,weight,value in self.columnDefs:
50             if value!=None:
51                 cols.append((label,value))
52             else:
53                 cols.append((label,available_space*weight/total_weights))
54         self.columns=cols
55
56     def refresh(self):
57         self.hb.commands = self.commands
58         self.hb.refresh()
59         self.w.box()
60         self.displayHeader()
61         for r in range(0,self.pageSize):
62             self.displayRow(r)
63         self.w.refresh()
64         self.highlight()
65
66     def clear(self):
67         self.w.erase()
68         self.w.refresh()
69
70     def centreChild(self,child):
71         (y,x)=child.getmaxyx()
72         child.mvwin(self.cy-y/2,self.cx-x/2)
73
74
75     def displayHeader(self):
76         cursor = 2
77         for header,width in self.columns:
78             self.w.addnstr(1,cursor,header+" "*width,width)
79             self.w.addstr(2,cursor,"-"*width)
80             cursor += width+1
81
82     def displayRow(self,row):
83         if self.topline+row < len(self.entries):
84             entry = self.entries[self.topline+row]
85             cursor = 2
86             if self.selected[self.topline+row]:
87                 self.w.addstr(row+3, 1, "*")
88             else:
89                 self.w.addstr(row+3, 1, " ")
90             for k,width in self.columns:
91                 if k.lower() in entry:
92                     self.w.addnstr(row+3,cursor,str(entry[k.lower()])+" "*width,width)
93                 cursor += width+1
94         else:
95             self.w.addstr(row+3,1," "*(self.mx-2))
96
97     def highlight(self):
98         row = self.hl-self.topline+3
99         if row > 1 and row < self.my:
100             self.w.chgat(row,1,self.mx-2,curses.A_REVERSE)
101
102     def unHighlight(self):
103         row = self.hl-self.topline+3
104         if row > 1 and row < self.my:
105             self.w.chgat(row,1,self.mx-2,curses.A_NORMAL)
106
107     def mvHighlight(self,delta):
108         new = self.hl+delta
109         new = max(new,0)
110         new = min(new,len(self.entries)-1)
111         self.unHighlight()
112         self.hl = new
113         self.highlight()
114     
115     def scroll(self,delta):
116         self.unHighlight()
117         self.topline += delta
118         self.topline = min(self.topline,len(self.entries)-1)
119         self.topline = max(self.topline,0)
120         self.refresh()
121
122     def search(self, string):
123         i = 0
124         found = False
125         for e in self.entries:
126             for k,v in e.items():
127                 if str(v).find(string) != -1:
128                     found = True
129             if found:
130                 break
131             i += 1;
132         if found:
133             self.last_search = string
134             self.search_index = i
135             return i
136         else:
137             self.search_index = -1
138             return -1
139
140     def findNext(self):
141         if self.last_search == "" or self.search_index == -1:
142             return -1
143         found = False
144         for i in range(self.hl+1,len(self.entries)-1):
145             for k,v in self.entries[i].items():
146                 if str(v).find(self.last_search) != -1:
147                     found = True
148             if found:
149                 break
150         if found:
151             self.search_index = i
152             return i
153         else:
154             return -1
155
156     def findPrevious(self):
157         if self.last_search == "" or self.search_index == -1:
158             return -1
159         found = False
160         for i in range(self.hl-1, 0, -1):
161             for k,v in self.entries[i].items():
162                 if str(v).find(self.last_search) != -1:
163                     found = True
164             if found:
165                 break
166         if found:
167             self.search_index = i
168             return i
169         else:
170             return -1
171
172
173     def eventLoop(self):
174         self.w.keypad(1)
175         self.refresh()
176
177         ch = self.w.getch()
178         while ch != 27 and ch != 113:
179             ch = self.handleInput(ch)
180             if ch==113:
181                 return {}
182             self.w.refresh()
183             ch = self.w.getch()
184             self.hb.refresh()
185
186     def handleInput(self,ch):
187         if ch == curses.KEY_UP or ch == 107 or ch == 16:
188             if self.hl == self.topline:
189                 self.scroll(-self.pageSize/2-1)
190             self.mvHighlight(-1)
191         elif ch == curses.KEY_DOWN or ch == 106 or ch == 14:
192             if self.hl == self.topline+self.pageSize-1:
193                 self.scroll(+self.pageSize/2+1)
194             self.mvHighlight(+1)
195         elif ch == curses.KEY_PPAGE:
196             self.scroll(-self.pageSize)
197             self.mvHighlight(-self.pageSize)
198         elif ch == curses.KEY_NPAGE:
199             self.scroll(+self.pageSize)
200             self.mvHighlight(+self.pageSize)
201         elif ch == 47: # forward slash
202             string = self.hb.getSearch()
203             hl = self.search(string)
204             if hl != -1:
205                 delta = hl - self.hl
206                 self.scroll(delta)
207                 self.mvHighlight(delta)
208             else:
209                 self.hb.display(string+' not found')
210         elif ch == 110: # n
211             hl = self.findNext()
212             if hl != -1:
213                 delta = hl - self.hl
214                 self.scroll(delta)
215                 self.mvHighlight(delta)
216             else:
217                 self.hb.display(self.last_search+' not found')
218         elif ch == 78: # N
219             hl = self.findPrevious()
220             if hl != -1:
221                 delta = hl - self.hl
222                 self.scroll(delta)
223                 self.mvHighlight(delta)
224             else:
225                 self.hb.display(self.last_search+' not found')
226         elif ch == 270: # F6 Sorts
227             w = curses.newwin(40,20,20,20)
228             cl = columnSelector(w,self.hb)
229             self.centreChild(w)
230             col = cl.eventLoop()
231             cl.clear()
232             self.sortByColumn(col)
233             self.clear()
234             self.refresh()
235         elif ch == 32:
236             if len(self.selected)>0:
237                 self.selected[self.hl] = not self.selected[self.hl]
238             self.displayRow(self.hl-self.topline)
239             self.highlight()
240
241
242
243 class trashBrowser(browserWindow):
244     columnDefs = [('ID',0,3),
245                   ('ISBN',0,13),
246                   ('Authors',30,None),
247                   ('Title',60,None)]
248     
249     cs = [(' r', 'restore selected'), (' d', 'delete selected')]
250     
251     # redefinable functions
252     def viewSelection(self,book):
253         bookid = book['id']
254         w=curses.newwin(1,1,20,20)
255         bf = bookForm(w,self.hb,book)
256         self.centreChild(w)
257         bf.caption='Viewing Book '+str(bookid)
258         bf.blabel='done'
259         bf.eventLoop()
260         bf.clear()
261
262     def restoreSelected(self):
263         books = []
264         for sel,book in zip(self.selected, self.entries):
265             if sel:
266                 books.append(book)
267         db.restoreBooks(books)
268
269     def delSelected(self):
270         books = []
271         for sel,book in zip(self.selected, self.entries):
272             if sel:
273                 books.append(book)
274         db.deleteBooks(books)
275
276     def refreshBooks(self):
277         self.entries = db.getRemovedBooks()
278         self.selected = map(lambda x:False, self.entries)
279
280     def handleInput(self,ch):
281         browserWindow.handleInput(self,ch)
282         if ch == 10:
283             book = self.entries[self.hl]
284             self.viewSelection(book)
285             self.refresh()
286         if ch==114: #restore books
287             count=0
288             for s in self.selected[0:self.hl-1]:
289                 if s:
290                     count+=1
291             self.restoreSelected()
292             self.refreshBooks()
293             self.refresh()
294             self.scroll(-count)
295             self.mvHighlight(-count)
296         if ch==100: # delete books
297             count=0
298             for s in self.selected[0:self.hl-1]:
299                 if s:
300                     count+=1
301             self.delSelected()
302             self.refreshBooks()
303             self.refresh()
304             self.scroll(-count)
305             self.mvHighlight(-count)
306         return ch
307
308 class bookBrowser(browserWindow):
309     columnDefs = [('ID',0,3),
310                   ('ISBN',0,13),
311                   ('Authors',30,None),
312                   ('Title',60,None)]
313     
314     cs = [(' u', 'update'), (' d', 'delete selected')]
315     
316     # redefinable functions
317     def updateSelection(self,book):
318         bookid = book['id']
319         
320         w=curses.newwin(1,1)
321         bf=bookForm(w,self.hb,book)
322         self.centreChild(w)
323         bf.caption='Update Book '+str(bookid)
324         bf.blabel='update'
325         newbook = bf.eventLoop()
326         if len(newbook)!=0:
327             db.updateBook(newbook,bookid)
328         bf.clear()
329
330     def viewSelection(self,book):
331         bookid = book['id']
332         w=curses.newwin(1,1,20,20)
333         bf = bookForm(w,self.hb,book)
334         self.centreChild(w)
335         bf.caption='Viewing Book '+str(bookid)
336         bf.blabel='done'
337         bf.eventLoop()
338         bf.clear()
339
340     def categorizeSelection(self,book):
341         w = curses.newwin(40,20,20,20)
342         cs = categorySelector(w,self.hb)
343         self.centreChild(w)
344         cs.book = book
345         cs.refreshCategories()
346         cs.eventLoop()
347         cs.clear()
348     
349     def delSelected(self):
350         books = []
351         for sel,book in zip(self.selected, self.entries):
352             if sel:
353                 books.append(book)
354         db.removeBooks(books)
355
356     def refreshBooks(self):
357         self.entries = db.getBooks()
358         self.selected = map(lambda x:False, self.entries)
359
360     def refreshBooksInCategory(self,cat):
361         self.entries = db.getBooksByCategory(cat)
362         self.selected = map(lambda x:False, self.entries)
363
364     def handleInput(self,ch):
365         browserWindow.handleInput(self,ch)
366         if ch == 117: #update on 'u'
367             book = self.entries[self.hl]
368             self.updateSelection(book)
369             self.entries[self.hl]=db.getBookByID(book['id'])
370             self.refresh()
371         elif ch == 10:
372             book = self.entries[self.hl]
373             self.viewSelection(book)
374             self.refresh()
375         elif ch == 99:
376             book = self.entries[self.hl]
377             self.categorizeSelection(book)
378             self.refresh()
379         if ch==100:
380             count=0
381             for s in self.selected[0:self.hl-1]:
382                 if s:
383                     count+=1
384             self.delSelected()
385             self.refreshBooks()
386             self.refresh()
387             self.scroll(-count)
388             self.mvHighlight(-count)
389         return ch
390
391 class categoryBrowser(browserWindow):
392     columnDefs = [('Category',100,None)]
393     cs = [(' a', 'add category'), (' d', 'delete selected')]
394
395
396     def refreshCategories(self):
397         self.entries = db.getCategories()
398         self.sortByColumn('category')
399         self.selected = map(lambda x:False, self.entries)
400
401     def addCategory(self):
402         w = curses.newwin(1,1,10,10)
403         cf = categoryForm(w,self.hb)
404         self.centreChild(w)
405         cats = cf.eventLoop()
406         for c in cats:
407             db.addCategory(c)
408         cf.clear()
409
410     def viewCategory(self):
411         w = curses.newwin(20,80,20,20)
412         b = bookBrowser(w,self.hb)
413         self.centreChild(w)
414         b.refreshBooksInCategory(self.entries[self.hl])
415         b.eventLoop()
416         b.clear()
417
418     def delSelected(self):
419         categories = []
420         for sel,cat in zip(self.selected, self.entries):
421             if sel:
422                 categories.append(cat)
423         db.deleteCategories(categories)
424
425     def handleInput(self,ch):
426         browserWindow.handleInput(self,ch)
427         if ch==97:
428             self.addCategory()
429             self.refreshCategories()
430             self.refresh()
431         if ch ==10:
432             self.viewCategory()
433             self.refresh()
434         if ch==100:
435             count=0
436             for s in self.selected[0:self.hl-1]:
437                 if s:
438                     count+=1
439             self.delSelected()
440             self.refreshCategories()
441             self.refresh()
442             self.scroll(-count)
443             self.mvHighlight(-count)
444         return ch
445
446 class categorySelector(browserWindow):
447     columnDefs = [('Category',100,None)]
448     cs = [(' a', 'add category'), (' c', 'commit')]
449     book = {'id':''}
450     original=[]
451
452
453     def refreshCategories(self):
454         self.entries = db.getCategories()
455         self.sortByColumn('category')
456         self.refreshSelected()
457
458     def refreshSelected(self):
459         self.original = map(lambda x:False, self.entries)
460         cats = db.getBookCategories(self.book)
461         cats.sort()
462         cats.sort(key=lambda k: k.get('category')) # key=dict.get(col))
463         i = 0
464         j = 0
465         for cat in self.entries:
466             if i == len(cats):
467                 break
468             if cat['id']==cats[i]['cat_id']:
469                 self.original[j] = True;
470                 i+=1
471             j+=1
472         self.selected = self.original[:]
473
474
475     def addCategory(self):
476         w = curses.newwin(1,1,10,10)
477         cf = categoryForm(w,self.hb)
478         self.centreChild(w)
479         cats = cf.eventLoop()
480         for c in cats:
481             db.addCategory(c)
482         cf.clear()
483
484     def updateCategories(self):
485         # first removed the deselected ones
486         uncats = []
487         cats = []
488         for old, new, category in zip(self.original, self.selected, self.entries):
489             if old and (not new):
490                 uncats.append(category)
491             if (not old) and new:
492                 cats.append(category)
493         db.uncategorizeBook(self.book, uncats)
494         # add the newly selected categories
495         db.categorizeBook(self.book, cats)
496
497
498     def handleInput(self,ch):
499         browserWindow.handleInput(self,ch)
500         if ch==97:
501             self.addCategory()
502             self.refreshCategories()
503             self.refresh()
504         if ch==99:
505             self.updateCategories()
506             return 113
507
508
509
510 class columnSelector(browserWindow):
511     columnDefs = [('Column',100,None)]
512     entries = [{'column': 'id'}, {'column': 'isbn'}, {'column': 'lccn'},
513             {'column': 'title'}, {'column': 'subtitle'}, {'column': 'authors'}, 
514             {'column': 'edition'}, {'column': 'publisher'}, 
515             {'column': 'publish year'}, {'column': 'publish month'}, 
516             {'column': 'publish location'}, {'column': 'pages'}, {'column': 'pagination'}, 
517             {'column': 'weight'}, {'column': 'last updated'}]
518
519     def __init__(self,window,helpbar):
520         self.selected = [False,False,False,False,False,False,False,
521                          False,False,False,False,False,False,False,False]
522         browserWindow.__init__(self,window,helpbar)
523
524
525     def eventLoop(self):
526         self.w.keypad(1)
527         self.refresh()
528
529         ch = self.w.getch()
530         while ch != 27 and ch != 113:
531             ch = self.handleInput(ch)
532             if ch==10:
533                 col = self.entries[self.hl]
534                 return col['column']
535             self.w.refresh()
536             ch = self.w.getch()
537             self.hb.refresh()
538     
539     def handleInput(self,ch):
540         browserWindow.handleInput(self,ch)
541         return ch