Categorizing and uncategorizing based on selection
[public/library.git] / library / interface / browser.py
1 import curses
2 import library.database as db
3 from library.interface.form import BookForm,BookView,CategoryForm,error_form,catch_error
4 from library.exceptions import NoHighlightedEntry
5
6 class browserWindow:
7     # These are actually class variables, not member variables? :<
8     _default_height=25
9     _default_width=60
10
11     hl=0
12     topline = 0
13     entries = []
14     selected = list()
15     commands = [(' /', 'search'), (' n', 'find next'), (' N', 'find previous'),
16                 ('F6', 'Sort Column'), (' q', 'quit'), ('Space', 'select'), ('Enter', 'view')]
17     cs = []
18     # column definitions are in (label, weight, specified width) triples
19     columnDefs = [('something',1,None)]
20     mx = my = 0
21     # for searches
22     last_search = ""
23     found_index = 0
24
25     def __init__(self,window,helpbar, height=0, width=0):
26         if not(height and width):
27             height = browserWindow._default_height
28             width = browserWindow._default_width
29         self.w = window
30         self.hb = helpbar
31         self.w.resize(height,width)
32         self.updateGeometry()
33         self.commands = self.cs+self.commands
34
35     def load_data(self, data=[]):
36         self.entries = data
37         self.selected = list(map(lambda x:False, self.entries))
38
39     def sortByColumn(self, col):
40         self.entries.sort(key=lambda k: k.get(col,"")) # key=dict.get(col))
41         self.selected = list(map(lambda x: False, self.selected))
42
43
44     def updateGeometry(self):
45         (self.my,self.mx)=self.w.getmaxyx()
46         self.pageSize = self.my-4
47         self.calcColWidths()
48
49     def calcColWidths(self):
50         total_weights = 0
51         available_space = self.mx - len(self.columnDefs) -2
52         cols = []
53         for label,weight,value in self.columnDefs:
54             if value!=None:
55                 available_space -= value
56             else:
57                 total_weights+=weight
58
59         for label,weight,value in self.columnDefs:
60             if value!=None:
61                 cols.append((label,value))
62             else:
63                 cols.append((label,available_space*weight//total_weights))
64         self.columns=cols
65
66     def refresh(self):
67         self.hb.commands = self.commands
68         self.hb.refresh()
69         self.w.box()
70         self.displayHeader()
71         for r in range(0,self.pageSize):
72             self.displayRow(r)
73         self.w.refresh()
74         self.highlight()
75
76     def clear(self):
77         self.w.erase()
78         self.w.refresh()
79
80     def centreChild(self,child):
81         (y,x) = self.w.getbegyx()
82         (r,c) = child.getmaxyx()
83         child.mvwin( y+(self.my-r)//2,x+(self.mx-c)//2)
84
85
86     def displayHeader(self):
87         cursor = 2
88         for header,width in self.columns:
89             self.w.addnstr(1,cursor,header+" "*width,width)
90             self.w.addstr(2,cursor,"-"*width)
91             cursor += width+1
92
93     def displayRow(self,row):
94         if self.topline+row < len(self.entries):
95             entry = self.entries[self.topline+row]
96             cursor = 2
97             self.w.addnstr(row+3, 1, " "*self.mx,self.mx-2)
98             if self.selected[self.topline+row]:
99                 self.w.addstr(row+3, 1, "*")
100             else:
101                 self.w.addstr(row+3, 1, " ")
102             for k,width in self.columns:
103                 if k.lower() in entry:
104                     self.w.addnstr(row+3, cursor,
105                                    str(entry[k.lower()])+" "*width, width)
106                 cursor += width+1
107         else:
108             self.w.addstr(row+3,1," "*(self.mx-2))
109
110     def highlight(self):
111         row = self.hl-self.topline+3
112         if row > 1 and row < self.my:
113             self.w.chgat(row,1,self.mx-2,curses.A_REVERSE)
114
115     def unHighlight(self):
116         row = self.hl-self.topline+3
117         if row > 1 and row < self.my:
118             self.w.chgat(row,1,self.mx-2,curses.A_NORMAL)
119
120     def mvHighlight(self,delta):
121         new = self.hl+delta
122         new = min(new,len(self.entries)-1)
123         new = max(new,0)
124         self.unHighlight()
125         self.hl = new
126         self.highlight()
127
128     def highlightedEntry(self):
129         try:
130             return self.entries[self.hl]
131         except:
132             raise NoHighlightedEntry()
133
134     def scroll(self,delta):
135         self.unHighlight()
136         self.topline += delta
137         self.topline = min(self.topline,len(self.entries)-self.pageSize)
138         self.topline = max(self.topline,0)
139         self.refresh()
140
141     def search(self, string):
142         case_sensitive = not(string.islower())
143         i = 0
144         found = False
145         for e in self.entries:
146             for k,v in e.items():
147                 # we or with found to make sure it is never "unfound"
148                 if case_sensitive:
149                     found = str(v).find(string) != -1 or found
150                 else:
151                     found = str(v).lower().find(string) != -1 or found
152             if found:
153                 break
154             i += 1;
155         if found:
156             self.last_search = string
157             self.search_index = i
158             self.case_sensitive = case_sensitive
159             return i
160         else:
161             self.search_index = -1
162             return -1
163
164     def _find_again(self, direction=1):
165         """Find the next match in the entries
166
167         direction = 1 means look ahead
168         direction = -1 means look back
169         """
170         if self.last_search == "" or self.search_index == -1:
171             return -1
172         found = False
173         if direction == 1:
174             last = len(self.entries) -1
175         elif direction == -1:
176             last = 0
177         for i in range(self.hl+direction, last, direction):
178             for k,v in self.entries[i].items():
179                 if self.case_sensitive:
180                     found = str(v).find(self.last_search) != -1 or found
181                 else:
182                     found = str(v).lower().find(self.last_search) != -1 or found
183             if found:
184                 break
185         if found:
186             self.search_index = i
187             return i
188         else:
189             return -1
190
191     def eventLoop(self):
192         self.w.keypad(1)
193         self.refresh()
194
195         ch = self.w.getch()
196         while ch != 27 and ch != 113:
197             ch = self.handleInput(ch)
198             if ch == 113:
199                 return {}
200             self.w.refresh()
201             ch = self.w.getch()
202             self.hb.refresh()
203
204     def handleInput(self,ch,isSortByColumn=False):
205         if ch == curses.KEY_UP or ch == 107 or ch == 16:
206             if self.hl == self.topline:
207                 self.scroll(-self.pageSize//2-1)
208             self.mvHighlight(-1)
209         elif ch == curses.KEY_DOWN or ch == 106 or ch == 14:
210             if self.hl == self.topline+self.pageSize-1:
211                 self.scroll(+self.pageSize//2+1)
212             self.mvHighlight(+1)
213         elif ch == curses.KEY_PPAGE:
214             self.scroll(-self.pageSize)
215             self.mvHighlight(-self.pageSize)
216         elif ch == curses.KEY_NPAGE:
217             self.scroll(+self.pageSize)
218             self.mvHighlight(+self.pageSize)
219         elif ch == curses.KEY_HOME:
220             self.scroll(-len(self.entries))
221             self.mvHighlight(-len(self.entries))
222         elif ch == curses.KEY_END:
223             self.scroll(len(self.entries))
224             self.mvHighlight(len(self.entries))
225         elif ch == 47: # forward slash
226             string = self.hb.getSearch()
227             hl = self.search(string)
228             if hl != -1:
229                 delta = hl - self.hl
230                 self.scroll(delta)
231                 self.mvHighlight(delta)
232             else:
233                 self.hb.display(string+' not found')
234         elif ch == 110: # n
235             hl = self._find_again(+1)
236             if hl != -1:
237                 delta = hl - self.hl
238                 self.scroll(delta)
239                 self.mvHighlight(delta)
240             else:
241                 self.hb.display(self.last_search+' not found')
242         elif ch == 78: # N
243             hl = self._find_again(-1)
244             if hl != -1:
245                 delta = hl - self.hl
246                 self.scroll(delta)
247                 self.mvHighlight(delta)
248             else:
249                 self.hb.display(self.last_search+' not found')
250         elif ch == 270 and not isSortByColumn: # F6 Sorts
251             w = curses.newwin(1,1)
252             cl = columnSelector(w,self.hb,40,20)
253             self.centreChild(w)
254             col = cl.eventLoop()
255             cl.clear()
256             self.sortByColumn(col)
257             self.clear()
258             self.refresh()
259         elif ch == 32 and not isSortByColumn:
260             if len(self.selected)>0:
261                 self.selected[self.hl] = not self.selected[self.hl]
262             self.displayRow(self.hl-self.topline)
263             self.highlight()
264
265
266
267 class trashBrowser(browserWindow):
268     columnDefs = [('ID',0,3),
269                   ('ISBN',0,13),
270                   ('Authors',30,None),
271                   ('Title',60,None)]
272
273     cs = [(' r', 'restore selected'), (' d', 'delete selected')]
274
275
276     # redefinable functions
277     def viewSelection(self,book):
278         bookid = book['id']
279         w=curses.newwin(1,1)
280         bf = BookView(w, self.hb, book, width=self.mx-10)
281         self.centreChild(w)
282         bf.event_loop()
283         bf.clear()
284
285     @catch_error
286     def restoreSelected(self):
287         books = []
288         for sel,book in zip(self.selected, self.entries):
289             if sel:
290                 books.append(book)
291         db.restoreBooks(books)
292
293     @catch_error
294     def delSelected(self):
295         books = []
296         for sel,book in zip(self.selected, self.entries):
297             if sel:
298                 books.append(book)
299         db.deleteBooks(books)
300
301     def refreshBooks(self):
302         self.load_data(db.getRemovedBooks())
303
304     @catch_error
305     def handleInput(self,ch):
306         browserWindow.handleInput(self,ch)
307         if ch == 10:
308             book = self.highlightedEntry()
309             self.viewSelection(book)
310             self.refresh()
311         if ch == 114: #restore books
312             count=0
313             for s in self.selected[0:self.hl-1]:
314                 if s:
315                     count+=1
316             self.restoreSelected()
317             self.refreshBooks()
318             self.refresh()
319             self.scroll(-count)
320             self.mvHighlight(-count)
321         if ch == 100: # delete books
322             count=0
323             for s in self.selected[0:self.hl-1]:
324                 if s:
325                     count+=1
326             self.delSelected()
327             self.refreshBooks()
328             self.refresh()
329             self.scroll(-count)
330             self.mvHighlight(-count)
331         return ch
332
333 class bookBrowser(browserWindow):
334     columnDefs = [('ID',0,3),
335                   ('ISBN',0,13),
336                   ('Authors',30,None),
337                   ('Title',60,None)]
338
339     cs = [(' u', 'update'), (' d', 'delete selected'), (' c', 'categorize'), (' a', 'add selected to category')]
340
341
342     # redefinable functions
343     @catch_error
344     def updateSelection(self,book):
345         bookid = book['id']
346
347         w=curses.newwin(1,1)
348         bf = BookForm(w,self.hb,book, width=self.mx-20)
349         self.centreChild(w)
350         bf.caption='Update Book '+str(bookid)
351         bf.blabel='update'
352         newbook = bf.event_loop()
353         bf.clear()
354         if len(newbook)!=0:
355             db.updateBook(newbook,bookid)
356
357     def viewSelection(self,book):
358         bookid = book['id']
359         w=curses.newwin(1,1)
360         bf = BookView(w,self.hb,book, width=self.mx-20)
361         self.centreChild(w)
362         bf.event_loop()
363         bf.clear()
364
365     def categorizeSelection(self,book):
366         w = curses.newwin(1,1)
367         cs = categorySelector(w,self.hb)
368         self.centreChild(w)
369         cs.book = book
370         cs.refreshCategories()
371         cs.eventLoop()
372         cs.clear()
373         self.refreshBooks()
374
375     @catch_error
376     def delSelected(self):
377         books = []
378         for sel,book in zip(self.selected, self.entries):
379             if sel:
380                 books.append(book)
381         db.removeBooks(books)
382
383     @catch_error
384     def addBooksToCategory(self):
385         books = []
386         for sel,book in zip(self.selected, self.entries):
387             if sel:
388                 books.append(book)
389         w = curses.newwin(1,1)
390         cs = singleCategorySelector(w,self.hb,40,50)
391         self.centreChild(w)
392         cs.refreshCategories()
393         cat = cs.eventLoop()
394         cs.clear()
395         self.refreshBooks()
396         if cat:
397             db.categorizeBooks(cat, books)
398
399     def refreshBooks(self):
400         self.load_data(db.get_books())
401
402     #def refreshBooksInCategory(self,cat):
403     #    self.refreshBooks = lambda : self.load_data(db.getBooksByCategory(cat))
404     #    self.refreshBooks()
405
406     def refreshBooksUncategorized(self):
407         self.refreshBooks = lambda : self.load_data(db.getUncategorizedBooks())
408         self.refreshBooks()
409
410     def refreshBooksCheckedout(self):
411         self.refreshBooks = lambda : self.load_data(db.get_checkedout_books())
412         self.refreshBooks()
413
414     def refreshBooksOnshelf(self):
415         self.refreshBooks = lambda : self.load_data(db.get_onshelf_books())
416         self.refreshBooks()
417
418     @catch_error
419     def handleInput(self,ch):
420         browserWindow.handleInput(self,ch)
421         if ch == 117: #update on 'u'
422             book = self.highlightedEntry()
423             self.updateSelection(book)
424             self.entries[self.hl]=db.get_book(book['id'])
425             self.refresh()
426         elif ch == 10:
427             book = self.highlightedEntry()
428             self.viewSelection(book)
429             self.refresh()
430         elif ch == 99: #c
431             book = self.highlightedEntry()
432             self.categorizeSelection(book)
433             self.refresh()
434         elif ch == 97: #a
435             self.addBooksToCategory()
436             self.refresh()
437         if ch == 100:
438             count=0
439             for s in self.selected[0:self.hl-1]:
440                 if s:
441                     count+=1
442             self.delSelected()
443             self.refreshBooks()
444             self.refresh()
445             self.scroll(-count)
446             self.mvHighlight(-count)
447         return ch
448
449 #a pretty ugly hack in order to be able to remove books from this category
450 class bookBrowserInCategory(bookBrowser):
451     cs = [(' u', 'update'), (' d', 'delete selected'), (' c', 'categorize'), (' a', 'add selected to category'), (' r', 'uncategorize selected')]
452
453     def refreshBooksInCategory(self,cat):
454         self.cat = cat
455         self.refreshBooks = lambda : self.load_data(db.getBooksByCategory(cat))
456         self.refreshBooks()
457
458     @catch_error
459     def uncategorizeBooks(self):
460         books = []
461         for sel,book in zip(self.selected, self.entries):
462             if sel:
463                 books.append(book)
464         db.uncategorizeBooks(books, self.cat)
465
466     def handleInput(self,ch):
467         if ch == 114: #r
468             self.uncategorizeBooks()
469             self.refreshBooks()
470             self.refresh()
471         bookBrowser.handleInput(self,ch)
472
473 class categoryBrowser(browserWindow):
474     columnDefs = [('Category',100,None)]
475     cs = [(' a', 'add category'), (' d', 'delete selected')]
476
477     def refreshCategories(self):
478         self.load_data(db.getCategories())
479         self.sortByColumn('category')
480
481     @catch_error
482     def addCategory(self):
483         w = curses.newwin(1,1,10,10)
484         cf = CategoryForm(w,self.hb)
485         self.centreChild(w)
486         cat = cf.event_loop()
487         cf.clear()
488         db.addCategory(cat)
489
490     def viewCategory(self):
491         w = curses.newwin(3,5)
492         b = bookBrowserInCategory(w,self.hb)
493         self.centreChild(w)
494         b.refreshBooksInCategory(self.highlightedEntry())
495         b.eventLoop()
496         b.clear()
497
498     @catch_error
499     def delSelected(self):
500         categories = []
501         for sel,cat in zip(self.selected, self.entries):
502             if sel:
503                 categories.append(cat)
504         db.deleteCategories(categories)
505
506     def handleInput(self,ch):
507         browserWindow.handleInput(self,ch)
508         if ch == 97:
509             self.addCategory()
510             self.refreshCategories()
511             self.refresh()
512         if ch == 10:
513             self.viewCategory()
514             self.refresh()
515         if ch == 100:
516             count=0
517             for s in self.selected[0:self.hl-1]:
518                 if s:
519                     count+=1
520             self.delSelected()
521             self.refreshCategories()
522             self.refresh()
523             self.scroll(-count)
524             self.mvHighlight(-count)
525         return ch
526
527 class categorySelector(browserWindow):
528     columnDefs = [('Category',100,None)]
529     cs = [(' a', 'commit & add category'), (' c', 'commit')]
530     book = {'id':''}
531     original=[]
532
533
534     def refreshCategories(self):
535         self.entries = db.getCategories()
536         self.sortByColumn('category')
537         self.refreshSelected()
538
539     def refreshSelected(self):
540         self.original = list(map(lambda x:False, self.entries))
541         cats = db.getBookCategories(self.book)
542         cats.sort(key=lambda k: k.get('category')) # key=dict.get(col))
543         i = 0
544         j = 0
545         for cat in self.entries:
546             if i == len(cats):
547                 break
548             if cat['id']==cats[i]['cat_id']:
549                 self.original[j] = True;
550                 i+=1
551             j+=1
552         self.selected = self.original[:]
553
554     @catch_error
555     def addCategory(self):
556         w = curses.newwin(1,1,10,10)
557         cf = CategoryForm(w,self.hb)
558         self.centreChild(w)
559         cat = cf.event_loop()
560         cf.clear()
561         db.addCategory(cat)
562
563     @catch_error
564     def updateCategories(self):
565         # first removed the deselected ones
566         uncats = []
567         cats = []
568         for old, new, category in zip(self.original, self.selected,
569                                       self.entries):
570             if old and not new:
571                 uncats.append(category)
572             if not old and new:
573                 cats.append(category)
574         db.uncategorizeBook(self.book, uncats)
575         # add the newly selected categories
576         db.categorizeBook(self.book, cats)
577
578
579     def handleInput(self,ch):
580         browserWindow.handleInput(self,ch)
581         if ch == 97:
582             self.updateCategories()
583             self.addCategory()
584             self.refreshCategories()
585             self.refresh()
586         if ch == 99:
587             self.updateCategories()
588             return 113
589
590
591 class singleCategorySelector(categorySelector):
592     columnDefs = [('Category',100,None)]
593     cs = []
594     commands = [(' /', 'search'), (' n', 'find next'), (' N', 'find previous'),
595                 (' q', 'quit'), ('Enter', 'done')]
596
597     def handleInput(self,ch):
598         return browserWindow.handleInput(self,ch,True)
599
600     def eventLoop(self):
601         self.w.keypad(1)
602         self.refresh()
603
604         ch = self.w.getch()
605         while ch != 27 and ch != 113:
606             if ch == 10:
607                 return self.highlightedEntry()
608             self.handleInput(ch)
609             self.w.refresh()
610             ch = self.w.getch()
611             self.hb.refresh()
612
613 class columnSelector(browserWindow):
614     columnDefs = [('Column',100,None)]
615     commands = [(' /', 'search'), (' n', 'find next'), (' N', 'find previous'),
616                 (' q', 'quit'), ('Enter', 'done')]
617     entries = [
618             {'column': 'id'}, {'column': 'isbn'}, {'column': 'lccn'},
619             {'column': 'title'}, {'column': 'subtitle'}, {'column': 'authors'},
620             {'column': 'edition'}, {'column': 'publisher'}, 
621             {'column': 'publish year'}, {'column': 'publish month'}, 
622             {'column': 'publish location'}, {'column': 'pages'},
623             {'column': 'pagination'}, {'column': 'weight'},
624             {'column': 'last updated'},
625     ]
626
627     def __init__(self,window,helpbar,height=40,width=20):
628         self.selected = [False for _ in self.entries]
629         browserWindow.__init__(self,window,helpbar,height,width)
630
631
632     def eventLoop(self):
633         self.w.keypad(1)
634         self.refresh()
635
636         ch = self.w.getch()
637         while ch != 27 and ch != 113:
638             ch = self.handleInput(ch)
639             if ch == 10:
640                 col = self.highlightedEntry()
641                 return col['column']
642             self.w.refresh()
643             ch = self.w.getch()
644             self.hb.refresh()
645
646     def handleInput(self,ch):
647         browserWindow.handleInput(self,ch,True)
648         return ch