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