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