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