1
import gtk, gtk.glade, gobject, re, os, os.path
2
from gourmet import gglobals, convert
3
from gourmet.gtk_extras import WidgetSaver, mnemonic_manager, pageable_store
4
from gourmet.gtk_extras import cb_extras as cb
5
from gourmet.gtk_extras import dialog_extras as de
6
from gettext import gettext as _
7
from gettext import ngettext
8
#import nutrition.nutritionDruid as nutritionDruid
11
current_path = os.path.split(os.path.join(os.getcwd(),__file__))[0]
17
"""KeyEditor sets up a GUI to allow editing which keys correspond to which items throughout
18
the recipe database. It is useful for corrections or changes to keys en masse.
21
def __init__ (self, rd=None, rg=None):
22
self.glade = gtk.glade.XML(os.path.join(current_path,'keyeditor.glade'))
25
self.widget_names = ['treeview', 'searchByBox', 'searchEntry', 'searchButton', 'window',
26
'searchAsYouTypeToggle', 'regexpTog',
33
for w in self.widget_names:
34
setattr(self,w,self.glade.get_widget(w))
35
self.entries = {'ingkey':self.changeKeyEntry,
36
'item':self.changeItemEntry,
37
'unit':self.changeUnitEntry,
38
'amount':self.changeAmountEntry,
40
# setup entry callback to sensitize/desensitize apply
41
self.applyEntriesButton.set_sensitive(False)
42
self.clearEntriesButton.set_sensitive(False)
43
for e in self.entries.values():
44
e.connect('changed',self.entryChangedCB)
45
# Make our lovely model
47
# setup completion in entry
48
cb.make_completion(self.changeKeyEntry,self.rg.inginfo.key_model)
49
# Setup next/prev/first/last buttons for view
50
self.prev_button = self.glade.get_widget('prevButton')
51
self.next_button = self.glade.get_widget('nextButton')
52
self.first_button = self.glade.get_widget('firstButton')
53
self.last_button = self.glade.get_widget('lastButton')
54
self.showing_label = self.glade.get_widget('showingLabel')
55
self.prev_button.connect('clicked',lambda *args: self.treeModel.prev_page())
56
self.next_button.connect('clicked',lambda *args: self.treeModel.next_page())
57
self.first_button.connect('clicked',lambda *args: self.treeModel.goto_first_page())
58
self.last_button.connect('clicked',lambda *args: self.treeModel.goto_last_page())
61
self.search_by = _('Key')
64
self.treeview.set_model(self.treeModel)
65
self.treeview.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
66
#self.treeview.set_model(self.treeModel)
67
self.glade.signal_autoconnect({
68
'iSearch':self.isearchCB,
69
'search':self.searchCB,
70
'search_as_you_type_toggle':self.search_as_you_typeCB,
71
'applyEntries':self.applyEntriesCB,
72
'clearEntries':self.clearEntriesCB,
73
'close_window': lambda *args: self.window.hide(),
74
'editNutritionalInfo':self.editNutritionalInfoCB,
76
# setup mnemonic manager
77
self.mm = mnemonic_manager.MnemonicManager()
78
self.mm.sacred_cows.append('search for')
79
self.mm.add_glade(self.glade)
80
self.mm.add_treeview(self.treeview)
81
self.mm.fix_conflicts_peacefully()
82
# to set our regexp_toggled variable
83
cb.set_model_from_list(self.searchByBox, [_('key'),_('item'),_('unit')])
84
self.searchByBox.set_active(0)
85
self.dont_ask = self.rg.prefs.get('dontAskDeleteKey',False)
87
self.rg.conf.append(WidgetSaver.WidgetSaver(
88
self.searchAsYouTypeToggle,
89
self.rg.prefs.get('sautTog',
90
{'active':self.searchAsYouTypeToggle.get_active()}),
92
self.rg.conf.append(WidgetSaver.WidgetSaver(
94
self.rg.prefs.get('regexpTog',
95
{'active':self.regexpTog.get_active()}),
98
def dont_ask_cb (self, widget, *args):
99
self.dont_ask=widget.get_active()
100
self.rg.prefs['dontAskDeleteKey']=self.dont_ask
102
def setupTreeView (self):
108
for n,head in [[self.FIELD_COL,_('Field')],
109
[self.VALUE_COL,_('Value')],
110
[self.COUNT_COL,_('Count')],
111
[self.REC_COL, _('Recipes')],
112
[self.NUT_COL, _('Nutritional Info')],
114
if n == self.NUT_COL:
115
renderer = gtk.CellRendererToggle()
117
renderer = gtk.CellRendererText()
118
# If we have gtk > 2.8, set up text-wrapping
120
renderer.get_property('wrap-width')
124
renderer.set_property('wrap-mode',gtk.WRAP_WORD)
125
if n == self.FIELD_COL:
126
renderer.set_property('wrap-width',60)
127
elif n in [self.VALUE_COL,self.REC_COL]: renderer.set_property('wrap-width',250)
128
else: renderer.set_property('wrap-width',100)
129
if n==self.VALUE_COL:
130
renderer.set_property('editable',True)
131
renderer.connect('edited',self.tree_edited,n,head)
132
if n == self.NUT_COL:
133
col = gtk.TreeViewColumn(head, renderer, active=n, visible=n)
135
col = gtk.TreeViewColumn(head, renderer, text=n)
136
if n == self.VALUE_COL:
137
col.set_property('expand',True)
138
col.set_resizable(True)
139
self.treeview.append_column(col)
142
def tree_edited (self, renderer, path_string, text, n, head):
143
indices = path_string.split(':')
144
path = tuple( map(int, indices))
145
itr = self.treeModel.get_iter(path)
146
curdic,field = self.get_dic_describing_iter(itr)
147
value = curdic[field]
148
if value == text: return
150
key = curdic['ingkey']
151
if de.getBoolean(label=_('Change all keys "%s" to "%s"?')%(key,text),
152
sublabel=_("You won't be able to undo this action. If there are already ingredients with the key \"%s\", you won't be able to distinguish between those items and the items you are changing now."%text)
155
self.rd.ingredients_table,
159
self.rd.delete_by_criteria(
160
self.rd.keylookup_table,
164
if de.getBoolean(label=_('Change all items "%s" to "%s"?')%(curdic['item'],text),
165
sublabel=_("You won't be able to undo this action. If there are already ingredients with the item \"%s\", you won't be able to distinguish between those items and the items you are changing now.")%text
168
self.rd.ingredients_table,
173
unit = curdic['unit']; key = curdic['ingkey']; item = curdic['item']
174
val = de.getRadio(label='Change unit',
176
[_('Change _all instances of "%(unit)s" to "%(text)s"')%locals(),1],
177
[_('Change "%(unit)s" to "%(text)s" only for _ingredients "%(item)s" with key "%(key)s"')%locals(),2],
183
self.rd.ingredients_table,
189
self.rd.ingredients_table,
193
elif field=='amount':
194
amount = curdic['amount']; unit = curdic['unit']; key = curdic['ingkey']; item = curdic['item']
196
new_amount = convert.frac_to_float(text)
198
de.show_amount_error(text)
200
val = de.getRadio(label='Change amount',
202
[_('Change _all instances of "%(amount)s" %(unit)s to %(text)s %(unit)s')%locals(),1],
203
[_('Change "%(amount)s" %(unit)s to "%(text)s" %(unit)s only _where the ingredient key is %(key)s')%locals(),2],
204
[_('Change "%(amount)s" %(unit)s to "%(text)s" %(unit)s only where the ingredient key is %(key)s _and where the item is %(item)s')%locals(),3],
209
cond = {'unit':unit,'amount':amount}
211
cond = {'unit':unit,'amount':amount,'ingkey':key}
215
self.rd.ingredients_table,
216
{'unit':unit,'amount':convert.frac_to_float(amount)},
217
{'unit':unit,'amount':new_amount}
221
self.treeModel.set_value(itr, n, text)
224
def makeTreeModel (self):
225
self.treeModel = KeyStore(self.rd,per_page=self.rg.prefs.get('recipes_per_page',12))
226
#self.orig_view = self.treeModel.view
227
self.treeModel.connect('page-changed',self.model_changed_cb)
228
self.treeModel.connect('view-changed',self.model_changed_cb)
230
def resetTree (self):
231
self.search_string = 'NO ONE WOULD EVER SEARCH FOR THIS HACKISH STRING'
232
curpage = self.treeModel.page
234
self.treeModel.set_page(curpage)
237
"""Do the actual searching."""
238
last_search = self.search_string
239
self.search_string = self.searchEntry.get_text()
240
last_by = self.search_by
241
self.search_by = cb.cb_get_active_text(self.searchByBox)
242
last_regexp = self.use_regexp
243
self.use_regexp = self.regexpTog.get_active()
244
if (self.search_by==last_by and
245
self.search_string==last_search and
246
self.use_regexp==last_regexp):
247
# Don't do anything...
249
# RESET THE VIEW IF NEED BE
250
if (self.search_string.find(last_search)!=0 or
251
self.search_by != last_by or
252
self.use_regexp != last_regexp):
253
self.treeModel.reset_views()
254
if self.search_by == _('item'):
255
self.treeModel.limit_on_ingkey(self.search_string,
256
search_options={'use_regexp':self.use_regexp,}
258
elif self.search_by == _('key'):
259
self.treeModel.limit_on_item(self.search_string,
260
search_options={'use_regexp':self.use_regexp})
261
else: # self.search_by == _('unit'):
262
self.treeModel.limit(self.search_string,
264
search_options={'use_regexp':self.use_regexp}
267
def isearchCB (self, *args):
268
if self.searchAsYouTypeToggle.get_active():
271
def searchCB (self, *args):
274
def search_as_you_typeCB (self, *args):
275
if self.searchAsYouTypeToggle.get_active():
276
self.searchButton.hide()
277
else: self.searchButton.show()
280
def clearEntriesCB (self, *args):
281
for e in self.entries.values(): e.set_text('')
283
def get_dic_describing_iter (self, itr):
284
"""Handed an itr in our tree, return a dictionary describing
285
that row and the field described.
287
For example, if we get the row
291
We return {'ingkey':'Foo'},'ingkey'
298
We return {'ingkey':'Foo','item':'Bar'},'item'
300
field = self.treeModel.get_value(itr, self.FIELD_COL)
301
value = self.treeModel.get_value(itr, self.VALUE_COL)
302
if field==self.treeModel.KEY:
303
return {'ingkey':value},'ingkey'
304
elif field==self.treeModel.ITEM:
305
key = self.treeModel.get_value(
306
self.treeModel.iter_parent(itr),
309
return {'ingkey':key,'item':value},'item'
310
elif field==self.treeModel.UNIT:
311
item_itr = self.treeModel.iter_parent(itr)
312
key_itr = self.treeModel.iter_parent(item_itr)
313
item = self.treeModel.get_value(item_itr,self.VALUE_COL)
314
key = self.treeModel.get_value(key_itr,self.VALUE_COL)
316
return {'ingkey':key,'item':item,'unit':unit},'unit'
317
elif field==self.treeModel.AMOUNT:
318
unit_itr = self.treeModel.iter_parent(itr)
319
item_itr = self.treeModel.iter_parent(unit_itr)
320
key_itr = self.treeModel.iter_parent(item_itr)
321
unit = self.treeModel.get_value(unit_itr,self.VALUE_COL)
322
item = self.treeModel.get_value(item_itr,self.VALUE_COL)
323
key = self.treeModel.get_value(key_itr,self.VALUE_COL)
325
return {'ingkey':key,'item':item,'unit':unit,'amount':amount},'amount'
327
print 'WTF! WE SHOULD NEVER LAND HERE!',field,value
330
def applyEntriesCB (self, *args):
332
for k,e in self.entries.items():
337
newdic[k]=convert.frac_to_float(txt)
339
de.show_amount_error(txt)
344
print 'We called applyEntriesCB with no text -- that shouldn\'t be possible'
346
mod,rows = self.treeview.get_selection().get_selected_rows()
347
if not de.getBoolean(
348
label=_("Change all selected rows?"),
349
sublabel=(_('This action will not be undoable. Are you that for all %s selected rows, you want to set the following values:')%len(rows)
350
+ (newdic.has_key('ingkey') and _('\nKey to %s')%newdic['ingkey'] or '')
351
+ (newdic.has_key('item') and _('\nItem to %s')%newdic['item'] or '')
352
+ (newdic.has_key('unit') and _('\nUnit to %s')%newdic['unit'] or '')
353
+ (newdic.has_key('amount') and _('\nAmount to %s')%newdic['amount'] or ''))):
355
# Now actually apply our lovely new logic...
359
itr=self.treeModel.get_iter(path)
360
# We check to see if we've updated the parent of our iter,
361
# in which case the changes would already be inherited by
362
# the current row (i.e. if the tree has been expanded and
363
# all rows have been selected).
364
parent = mod.iter_parent(itr); already_updated = False
366
if parent in updated_iters:
367
already_updated = True
369
parent = mod.iter_parent(parent)
370
if already_updated: continue
371
# Now that we're sure we really need to update...
372
curdic,field = self.get_dic_describing_iter(itr)
373
curkey = self.treeModel.get_value(itr,self.VALUE_COL)
374
if not already_updated:
376
self.rd.ingredients_table,
380
if curdic.has_key('ingkey') and newdic.has_key('ingkey'):
381
self.rd.delete_by_criteria(
382
self.rd.keylookup_table,
383
{'ingkey':curdic['ingkey']}
386
#self.update_iter(itr,newdic) # A recursive method that
387
# # will set values for us and
388
# # our children as necessary
389
#updated_iters.append(itr)
391
def editNutritionalInfoCB (self, *args):
392
nid = nutritionDruid.NutritionInfoDruid(self.rg.nd, self.rg.prefs)
393
mod,rows = self.treeview.get_selection().get_selected_rows()
396
itr = mod.get_iter(path)
397
# Climb to the key-level for each selection -- we don't
398
# care about anything else.
399
parent = mod.iter_parent(itr)
402
parent = mod.iter_parent(itr)
403
curkey = mod.get_value(itr,self.VALUE_COL)
404
#if mod.get_value(itr,self.NUT_COL):
405
# print "We can't yet edit nutritional information..."
408
keys_to_update[curkey]=[]
409
child = mod.iter_children(itr)
411
grandchild = mod.iter_children(child)
413
# Grand children are units...
414
unit = mod.get_value(grandchild,self.VALUE_COL)
416
greatgrandchild = mod.iter_children(grandchild)
417
while greatgrandchild:
418
amount = mod.get_value(
422
keys_to_update[curkey].append((convert.frac_to_float(amount),unit))
423
greatgrandchild = mod.iter_next(greatgrandchild)
424
grandchild = mod.iter_next(grandchild)
425
child = mod.iter_next(child)
426
nid.add_ingredients(keys_to_update.items())
427
nid.connect('finish',self.update_nutinfo)
430
def update_nutinfo (self, *args):
431
print 'KeyEditor.update_nutinfo'
432
self.treeModel.reset_views()
434
def update_iter (self, itr, newdic):
435
"""Update iter and its children based on values in newdic"""
436
field = self.treeModel.get_value(itr,self.FIELD_COL)
437
if newdic.has_key('item') and field==self.treeModel.ITEM:
438
self.treeModel.set_value(itr,self.VALUE_COL,newdic['item'])
439
elif newdic.has_key('ingkey') and field==self.treeModel.KEY:
440
self.treeModel.set_value(itr,self.VALUE_COL,newdic['ingkey'])
441
elif newdic.has_key('unit') and field==self.treeModel.UNIT:
442
self.treeModel.set_value(itr,self.VALUE_COL,newdic['unit'])
443
elif newdic.has_key('amount') and field==self.treeModel.AMOUNT:
444
self.treeModel.set_value(itr,self.VALUE_COL,newdic['amount'])
445
c = self.treeModel.iter_children(itr)
447
self.update_iter(c,newdic)
448
c = self.treeModel.iter_next(c)
450
def entryChangedCB (self, *args):
451
"""Set sensitivity of apply and clear buttons.
453
We are sensitive if we have text to apply or clear"""
454
for e in self.entries.values():
456
self.applyEntriesButton.set_sensitive(True)
457
self.clearEntriesButton.set_sensitive(True)
459
self.applyEntriesButton.set_sensitive(False)
460
self.clearEntriesButton.set_sensitive(False)
462
def reset_tree (self):
463
self.treeModel.reset_views()
464
self.search_by = None
465
self.search_string = ''
469
def model_changed_cb (self, model):
471
self.prev_button.set_sensitive(False)
472
self.first_button.set_sensitive(False)
474
self.prev_button.set_sensitive(True)
475
self.first_button.set_sensitive(True)
476
if model.get_last_page()==model.page:
477
self.next_button.set_sensitive(False)
478
self.last_button.set_sensitive(False)
480
self.next_button.set_sensitive(True)
481
self.last_button.set_sensitive(True)
482
self.update_showing_label()
484
def update_showing_label (self):
485
bottom,top,total = self.treeModel.showing()
486
if top >= total and bottom==1:
487
lab = ngettext('%s ingredient','%s ingredients',top)%top
489
# Do not translate bottom, top and total -- I use these fancy formatting
490
# strings in case your language needs the order changed!
491
lab = _('Showing ingredients %(bottom)s to %(top)s of %(total)s'%locals())
492
self.showing_label.set_markup('<i>' + lab + '</i>')
495
class KeyStore (pageable_store.PageableTreeStore,pageable_store.PageableViewStore):
496
"""A ListStore to show our beautiful keys.
499
'view-changed':(gobject.SIGNAL_RUN_LAST,
507
AMOUNT = _('Amount')+':'
509
columns = ['obj','ingkey','item','count','recipe','ndbno']
510
def __init__ (self, rd, per_page=15):
512
pageable_store.PageableTreeStore.__init__(self,
513
[gobject.TYPE_PYOBJECT, # row ref
518
int, # nutritional information equivalent
522
def reset_views (self):
523
self.view = self.rd.get_ingkeys_with_count()
524
for n in range(self._get_length_()):
528
itr = self.get_iter(path)
530
print 'Trouble with path',path
532
self.emit('row-changed',path,itr)
533
child = self.iter_children(itr)
535
path = self.get_path(child)
536
self.emit('row-changed',path,child)
537
child = self.iter_next(child)
538
#self.keylookup_table = self.rd.filter(self.rd.keylookup_table,lambda row: row.item)
539
# Limit ingredients_table to ingkeys only, then select the unique values of that, then
540
# filter ourselves to values that have keys
541
#self.view = self.rd.filter(self.rd.ingredients_table.project(self.rd.ingredients_table.ingkey).unique(),
542
# lambda foo: foo.ingkey)
544
def _setup_parent_ (self, *args, **kwargs):
547
def limit_on_ingkey (self, txt, search_options={}):
548
self.limit(txt,self.rd.ingredients_table+'.ingkey',search_options)
550
def limit_on_item (self, txt, search_options={}):
551
self.limit(txt,'item',search_options)
553
def limit (self, txt, column='ingkey', search_options={}):
554
if not txt: self.reset_views()
555
if search_options['use_regexp']:
556
s = {'search':txt,'operator':'REGEXP'}
558
s = {'search':'%'+txt.replace('%','%%')+'%','operator':'LIKE'}
560
self.change_view(self.rd.get_ingkeys_with_count(s))
562
def _get_length_ (self):
563
return len(self.view)
565
get_last_page = pageable_store.PageableViewStore.get_last_page
567
def _get_slice_ (self, bottom, top):
568
return [self.get_row(i) for i in self.view[bottom:top]]
570
def _get_item_ (self, path):
571
return self.get_row(self.view[indx])
573
def get_row (self, row):
577
# avoidable slowdown (look here if code seems sluggish)
583
def _get_children_ (self,itr):
585
field = self.get_value(itr,1)
586
value = self.get_value(itr,2)
589
for item in self.rd.get_unique_values('item',self.rd.ingredients_table,ingkey=ingkey):
593
self.rd.fetch_len(self.rd.ingredients_table,ingkey=ingkey,item=item),
594
self.get_recs(ingkey,item),
597
elif field==self.ITEM:
598
ingkey = self.get_value(self.iter_parent(itr),2)
600
for unit in self.rd.get_unique_values('unit',self.rd.ingredients_table,ingkey=ingkey,item=item):
604
self.rd.fetch_len(self.rd.ingredients_table,ingkey=ingkey,item=item,unit=unit),
611
self.get_value(self.iter_parent(itr),3),
614
elif field==self.UNIT:
615
item = self.get_value(self.iter_parent(itr),2)
616
ingkey = self.get_value(self.iter_parent(
617
self.iter_parent(itr)),2)
618
unit = self.get_value(itr,2)
620
for i in self.rd.fetch_all(self.rd.ingredients_table,ingkey=ingkey,item=item,unit=unit):
621
astring = self.rd.get_amount_as_string(i)
622
if astring in amounts: continue
627
and self.rd.fetch_len(self.rd.ingredients_table,
628
ingkey=ingkey,item=item,
630
amount=i.amount,rangeamount=i.rangeamount)
631
or self.rd.fetch_len(self.rd.ingredients_table,
632
ingkey=ingkey,item=item,
637
amounts.append(astring)
642
self.get_value(self.iter_parent(itr),3),
652
# self.get_recs(row.ingkey,subrow.item)] for subrow in row.grouped]
655
def get_recs (self, key, item):
656
"""Return a string with a list of recipes containing an ingredient with key and item"""
657
recs = [i.id for i in self.rd.fetch_all(self.rd.ingredients_table,ingkey=key,item=item)]
661
if r_id in looked_at: continue
662
rec = self.rd.get_rec(r_id)
664
titles.append(rec.title)
665
return ", ".join(titles)
667
if gtk.pygtk_version[1]<8:
668
gobject.type_register(KeyStore)
670
if __name__ == '__main__':
672
rm = recipeManager.default_rec_manager()
674
sys.path.append(os.path.realpath('../tests'))
676
rg = testExtras.FakeRecGui(rm)
678
ke.window.connect('delete-event',gtk.main_quit)