~vorlon/ubuntu/saucy/gourmet/trunk

« back to all changes in this revision

Viewing changes to src/lib/legacy_db/db_085/rdatabase.py

  • Committer: Bazaar Package Importer
  • Author(s): Rolf Leggewie
  • Date: 2008-07-26 13:29:41 UTC
  • Revision ID: james.westby@ubuntu.com-20080726132941-6ldd73qmacrzz0bn
Tags: upstream-0.14.0
ImportĀ upstreamĀ versionĀ 0.14.0

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
import os.path
 
2
from gourmet.gdebug import debug, TimeAction
 
3
import re, pickle, string, os.path, string
 
4
from gettext import gettext as _
 
5
import gourmet.gglobals
 
6
from gourmet import Undo, keymanager, convert, shopping
 
7
from gourmet.defaults import lang as defaults
 
8
 
 
9
# This is our base class for rdatabase.  All functions needed by
 
10
# Gourmet to access the database should be defined here and
 
11
# implemented by subclasses.  This was designed around metakit, so
 
12
# Gourmet requires an object-attribute style syntax for accessing
 
13
# database information.  For the time being, this is built in
 
14
# throughout Gourmet. Non-metakit backends, such as sql, have to
 
15
# implement this syntax themselves (which is fine by me because that
 
16
# means I have abstracted ways to handle e.g. SQLite in the future). See
 
17
# PythonicSQL.py and other files for the glue between SQL and the object/attribute
 
18
# style required by Gourmet.
 
19
 
 
20
class RecData:
 
21
 
 
22
    """RecData is our base class for handling database connections.
 
23
 
 
24
    Subclasses implement specific backends, such as metakit, sqlite, etc."""
 
25
    # constants for determining how to get amounts when there are ranges.
 
26
    AMT_MODE_LOW = 0
 
27
    AMT_MODE_AVERAGE = 1
 
28
    AMT_MODE_HIGH = 2
 
29
    
 
30
    RECIPE_TABLE_DESC = ('recipe',
 
31
                  [('id',"char(75)"),
 
32
                   ('title',"text"),
 
33
                   ('instructions',"text"),
 
34
                   ('modifications',"text"),
 
35
                   ('cuisine',"text"),
 
36
                   ('rating',"text"),
 
37
                   ('description',"text"),
 
38
                   ('category',"text"),
 
39
                   ('source',"text"),
 
40
                   ('preptime',"char(50)"),
 
41
                   ('cooktime',"char(50)"),                                       
 
42
                   ('servings',"char(50)"),
 
43
                   ('image',"binary"),
 
44
                   ('thumb','binary'),
 
45
                   ('deleted','bool'),
 
46
                   ],                   #'id' # key
 
47
                         ) 
 
48
    INGREDIENTS_TABLE_DESC=('ingredients',
 
49
                [('id','char(75)'),
 
50
                 ('refid','char(75)'),
 
51
                 ('unit','text'),
 
52
                 ('amount','float'),
 
53
                 ('rangeamount','float'),
 
54
                 ('item','text'),
 
55
                 ('ingkey','char(200)'),
 
56
                 ('optional','bool'),
 
57
                 ('shopoptional','int'), #Integer so we can distinguish unset from False
 
58
                 ('inggroup','char(200)'),
 
59
                 ('position','int'),
 
60
                 ('deleted','bool'),
 
61
                 ],
 
62
                )
 
63
    SHOPCATS_TABLE_DESC = ('shopcats',
 
64
                  [('shopkey','char(50)'),
 
65
                   ('category','char(200)'),
 
66
                   ('position','int')],
 
67
                  'shopkey' #key
 
68
                  )
 
69
    SHOPCATSORDER_TABLE_DESC = ('shopcatsorder',
 
70
                   [('category','char(50)'),
 
71
                    ('position','int'),
 
72
                    ],
 
73
                   'category' #key
 
74
                   )
 
75
    PANTRY_TABLE_DESC = ('pantry',
 
76
                  [('itm','char(200)'),
 
77
                   ('pantry','char(10)')],
 
78
                  'itm' #key
 
79
                  )
 
80
    CATEGORIES_TABLE_DESC = ("categories",
 
81
                     [('id','char(200)'),
 
82
                      ('type','char(100)'),
 
83
                      ('description','text')], 'id' #key
 
84
                     )
 
85
    DENSITY_TABLE_DESC = ("density",
 
86
                   [('dkey','char(150)'),
 
87
                    ('value','char(150)')],'dkey' #key
 
88
                   )
 
89
    CROSSUNITDICT_TABLE_DESC = ("crossunitdict",
 
90
                   [('cukey','char(150)'),
 
91
                    ('value','char(150)')],'cukey' #key
 
92
                   )
 
93
    UNITDICT_TABLE_DESC = ("unitdict",
 
94
                  [('ukey','char(150)'),
 
95
                   ('value','char(150)')],'ukey' #key
 
96
                  )
 
97
    CONVTABLE_TABLE_DESC = ("convtable",
 
98
                  [('ckey','char(150)'),
 
99
                   ('value','char(150)')],'ckey' #key
 
100
                  )
 
101
    
 
102
    def __init__ (self):
 
103
        timer = TimeAction('initialize_connection + setup_tables',0)        
 
104
        self.initialize_connection()
 
105
        self.setup_tables()        
 
106
        timer.end()
 
107
        self.top_id = {'r': 1}
 
108
        # hooks run after adding, modifying or deleting a recipe.
 
109
        # Each hook is handed the recipe, except for delete_hooks,
 
110
        # which is handed the ID (since the recipe has been deleted)
 
111
        self.add_hooks = []
 
112
        self.modify_hooks = []
 
113
        self.delete_hooks = []
 
114
        self.add_ing_hooks = []
 
115
 
 
116
    def initialize_connection (self):
 
117
        """Initialize our database connection."""
 
118
        raise NotImplementedError
 
119
 
 
120
    def save (self):
 
121
        """Save our database (if we have a separate 'save' concept)"""
 
122
        pass
 
123
 
 
124
    def setup_tables (self):
 
125
        """Setup all of our tables by calling setup_table for each one.
 
126
 
 
127
        Subclasses should do any necessary adjustments/tweaking before calling
 
128
        this function."""
 
129
        self.recipe_table = self.setup_table(*self.RECIPE_TABLE_DESC)
 
130
        self.ingredients_table = self.setup_table(*self.INGREDIENTS_TABLE_DESC)
 
131
        self.ingredients_table_not_deleted = self.ingredients_table.select(deleted=False)
 
132
        self.ingredients_table_deleted = self.ingredients_table.select(deleted=True)
 
133
        self.shopcats_table = self.setup_table(*self.SHOPCATS_TABLE_DESC)
 
134
        self.shopcatsorder_table = self.setup_table(*self.SHOPCATSORDER_TABLE_DESC)
 
135
        self.pantry_table = self.setup_table(*self.PANTRY_TABLE_DESC) 
 
136
        self.metaview = self.setup_table(*self.CATEGORIES_TABLE_DESC)
 
137
        # converter items
 
138
        self.density_table = self.setup_table(*self.DENSITY_TABLE_DESC)
 
139
        self.convtable_table = self.setup_table(*self.CONVTABLE_TABLE_DESC)
 
140
        self.crossunitdict_table = self.setup_table(*self.CROSSUNITDICT_TABLE_DESC)
 
141
        self.unitdict_table = self.setup_table(*self.UNITDICT_TABLE_DESC)
 
142
        
 
143
    def setup_table (self, name, data, key):
 
144
        """Create and return an object representing a table/view of our database.
 
145
 
 
146
        Name is the name of our table.
 
147
        Data is a list of tuples of column names and data types.
 
148
        Key is the column of the table that should be indexed.
 
149
        """
 
150
        raise NotImplementedError
 
151
 
 
152
    def run_hooks (self, hooks, *args):
 
153
        for h in hooks:
 
154
            debug('running hook %s with args %s'%(h,args),3)
 
155
            h(*args)
 
156
 
 
157
    def get_dict_for_obj (self, obj, keys):
 
158
        orig_dic = {}
 
159
        for k in keys:
 
160
            v=getattr(obj,k)
 
161
            orig_dic[k]=v
 
162
        return orig_dic
 
163
 
 
164
    def undoable_modify_rec (self, rec, dic, history=[], get_current_rec_method=None,
 
165
                             select_change_method=None):
 
166
        """Modify our recipe and remember how to undo our modification using history."""
 
167
        orig_dic = self.get_dict_for_obj(rec,dic.keys())
 
168
        reundo_name = "Re_apply"
 
169
        reapply_name = "Re_apply "
 
170
        reundo_name += string.join(["%s <i>%s</i>"%(k,v) for k,v in orig_dic.items()])
 
171
        reapply_name += string.join(["%s <i>%s</i>"%(k,v) for k,v in dic.items()])
 
172
        redo,reundo=None,None
 
173
        if get_current_rec_method:
 
174
            def redo (*args):
 
175
                r=get_current_rec_method()
 
176
                odic = self.get_dict_for_obj(r,dic.keys())
 
177
                return ([r,dic],[r,odic])
 
178
            def reundo (*args):
 
179
                r = get_current_rec_method()
 
180
                odic = self.get_dict_for_obj(r,orig_dic.keys())
 
181
                return ([r,orig_dic],[r,odic])
 
182
 
 
183
        def action (*args,**kwargs):
 
184
            """Our actual action allows for selecting changes after modifying"""
 
185
            self.modify_rec(*args,**kwargs)
 
186
            if select_change_method:
 
187
                select_change_method(*args,**kwargs)
 
188
                
 
189
        obj = Undo.UndoableObject(action,action,history,
 
190
                                  action_args=[rec,dic],undo_action_args=[rec,orig_dic],
 
191
                                  get_reapply_action_args=redo,
 
192
                                  get_reundo_action_args=reundo,
 
193
                                  reapply_name=reapply_name,
 
194
                                  reundo_name=reundo_name,)
 
195
        obj.perform()
 
196
 
 
197
    def modify_rec (self, rec, dict):
 
198
        """Modify recipe 'rec' based on a dictionary of properties and new values."""
 
199
        raise NotImplementedError
 
200
 
 
201
    def search (self, table, colname, text, exact=0, use_regexp=True):
 
202
 
 
203
        """Search colname of table for text, optionally using regular
 
204
        expressions and/or requiring an exact match."""
 
205
 
 
206
        raise NotImplementedError
 
207
 
 
208
    def get_default_values (self, colname):
 
209
        try:
 
210
            return defaults.fields[colname]
 
211
        except:
 
212
            return []
 
213
 
 
214
    def get_unique_values (self, colname,table=None):
 
215
        if not table: table=self.recipe_table
 
216
        dct = {}
 
217
        if defaults.fields.has_key(colname):
 
218
            for v in defaults.fields[colname]:
 
219
                dct[v]=1
 
220
        def add_to_dic (row):
 
221
            a=getattr(row,colname)
 
222
            if type(a)==type(""):
 
223
                for i in a.split(","):
 
224
                    dct[i.strip()]=1
 
225
            else:
 
226
                dct[a]=1
 
227
        table.filter(add_to_dic)
 
228
        return dct.keys()
 
229
 
 
230
    def get_ings (self, rec):
 
231
        """Handed rec, return a list of ingredients.
 
232
 
 
233
        rec should be an ID or an object with an attribute ID)"""
 
234
        if hasattr(rec,'id'):
 
235
            id=rec.id
 
236
        else:
 
237
            id=rec
 
238
        return self.ingredients_table.select(id=id,deleted=False)
 
239
 
 
240
    def order_ings (self, ingredients_table):
 
241
        """Handed a view of ingredients, we return an alist:
 
242
        [['group'|None ['ingredient1', 'ingredient2', ...]], ... ]
 
243
        """
 
244
        defaultn = 0
 
245
        groups = {}
 
246
        group_order = {}
 
247
        for i in ingredients_table:
 
248
            # defaults
 
249
            if not hasattr(i,'inggroup'):
 
250
                group=None
 
251
            else:
 
252
                group=i.inggroup
 
253
            if not hasattr(i,'position'):
 
254
                i.position=defaultn
 
255
                defaultn += 1
 
256
            if groups.has_key(group): 
 
257
                groups[group].append(i)
 
258
                # the position of the group is the smallest position of its members
 
259
                # in other words, positions pay no attention to groups really.
 
260
                if i.position < group_order[group]: group_order[group]=i.position
 
261
            else:
 
262
                groups[group]=[i]
 
263
                group_order[group]=i.position
 
264
        # now we just have to sort an i-listify
 
265
        def sort_groups (x,y):
 
266
            if group_order[x[0]] > group_order[y[0]]: return 1
 
267
            elif group_order[x[0]] == group_order[y[0]]: return 0
 
268
            else: return -1
 
269
        alist=groups.items()
 
270
        alist.sort(sort_groups)
 
271
        def sort_ings (x,y):
 
272
            if x.position > y.position: return 1
 
273
            elif x.position == y.position: return 0
 
274
            else: return -1
 
275
        for g,lst in alist:
 
276
            lst.sort(sort_ings)
 
277
        return alist
 
278
 
 
279
    def ingview_to_lst (self, view):
 
280
        """Handed a view of ingredient data, we output a useful list.
 
281
        The data we hand out consists of a list of tuples. Each tuple contains
 
282
        amt, unit, key, alternative?"""
 
283
        for i in view:
 
284
            ret.append([self.get_amount(i), i.unit, i.ingkey,])
 
285
        return ret
 
286
    
 
287
    def ing_shopper (self, view):
 
288
        return mkShopper(self.ingview_to_lst(view))
 
289
 
 
290
    def get_amount (self, ing, mult=1):
 
291
        """Given an ingredient object, return the amount for it.
 
292
 
 
293
        Amount may be a tuple if the amount is a range, a float if
 
294
        there is a single amount, or None"""
 
295
        amt=getattr(ing,'amount')
 
296
        ramt = getattr(ing,'rangeamount')
 
297
        if mult != 1:
 
298
            if amt: amt = amt * mult
 
299
            if ramt: ramt = ramt * mult
 
300
        if ramt:
 
301
            return (amt,ramt)
 
302
        else:
 
303
            return amt
 
304
 
 
305
    def get_amount_and_unit (self, ing, mult=1, conv=None,fractions=convert.FRACTIONS_ALL):
 
306
        """Return a tuple of strings representing our amount and unit.
 
307
        
 
308
        If we are handed a converter interface, we will adjust the
 
309
        units to make them readable.
 
310
        """
 
311
        amt=self.get_amount(ing,mult)
 
312
        unit=ing.unit
 
313
        ramount = None
 
314
        if type(amt)==tuple: amt,ramount = amt
 
315
        if conv:
 
316
            amt,unit = conv.adjust_unit(amt,unit)
 
317
            if ramount and unit != ing.unit:
 
318
                # if we're changing units... convert the upper range too
 
319
                ramount = ramount * conv.converter(ing.unit, unit)
 
320
        if ramount: amt = (amt,ramount)
 
321
        return (self._format_amount_string_from_amount(amt,fractions=fractions),unit)
 
322
        
 
323
    def get_amount_as_string (self,
 
324
                              ing,
 
325
                              mult=1,
 
326
                              fractions=convert.FRACTIONS_ALL
 
327
                              ):
 
328
        """Return a string representing our amount.
 
329
        If we have a multiplier, multiply the amount before returning it.        
 
330
        """
 
331
        amt = self.get_amount(ing,mult)
 
332
        return self._format_amount_string_from_amount(amt, fractions=fractions)
 
333
 
 
334
    def _format_amount_string_from_amount (self, amt, fractions=convert.FRACTIONS_ALL):
 
335
        """Format our amount string given an amount tuple.
 
336
 
 
337
        If you're thinking of using this function from outside, you
 
338
        should probably just use a convenience function like
 
339
        get_amount_as_string or get_amount_and_unit
 
340
        """
 
341
        if type(amt)==tuple:
 
342
            return "%s-%s"%(convert.float_to_frac(amt[0],fractions=fractions).strip(),
 
343
                            convert.float_to_frac(amt[1],fractions=fractions).strip())
 
344
        elif type(amt)==float:
 
345
            return convert.float_to_frac(amt,fractions=fractions)
 
346
        else: return ""
 
347
 
 
348
    def get_amount_as_float (self, ing, mode=1): #1 == self.AMT_MODE_AVERAGE
 
349
        """Return a float representing our amount.
 
350
 
 
351
        If we have a range for amount, this function will ignore the range and simply
 
352
        return a number.  'mode' specifies how we deal with the mode:
 
353
        self.AMT_MODE_AVERAGE means we average the mode (our default behavior)
 
354
        self.AMT_MODE_LOW means we use the low number.
 
355
        self.AMT_MODE_HIGH means we take the high number.
 
356
        """
 
357
        amt = self.get_amount(ing)
 
358
        if type(amt) in [float, type(None)]:
 
359
            return amt
 
360
        else:
 
361
            # otherwise we do our magic
 
362
            amt=list(amt)
 
363
            amt.sort() # make sure these are in order
 
364
            low,high=amt
 
365
            if mode==self.AMT_MODE_AVERAGE: return (low+high)/2.0
 
366
            elif mode==self.AMT_MODE_LOW: return low
 
367
            elif mode==self.AMT_MODE_HIGH: return high # mode==self.AMT_MODE_HIGH
 
368
            else:
 
369
                raise ValueError("%s is an invalid value for mode"%mode)
 
370
 
 
371
    def get_referenced_rec (self, ing):
 
372
        """Get recipe referenced by ingredient object."""
 
373
        if hasattr(ing,'refid') and ing.refid:
 
374
            rec = self.get_rec(ing.refid)
 
375
            if rec: return rec
 
376
        # otherwise, our reference is no use! Something's been
 
377
        # foobared. Unfortunately, this does happen, so rather than
 
378
        # screwing our user, let's try to look based on title/item
 
379
        # name (the name of the ingredient *should* be the title of
 
380
        # the recipe, though the user could change this)
 
381
        if hasattr(ing,'item'):
 
382
            recs=self.search(self.recipe_table,'title',ing.item,exact=True,use_regexp=False)
 
383
            if len(recs)==0:
 
384
                self.modify_ing(ing,{'idref':recs[0].id})
 
385
                return recs[0]
 
386
            else:
 
387
                debug("""Warning: there is more than one recipe titled"%(title)s"
 
388
                and our id reference to %(idref)s failed to match any
 
389
                recipes.  We are going to assume recipe ID %(id)s is
 
390
                the correct one."""%{'title':ing.item,
 
391
                                     'idref':ing.refid,
 
392
                                     'id':recs[0].id},
 
393
                      0)
 
394
                return recs[0]
 
395
    
 
396
    def get_rec (self, id, recipe_table=None):
 
397
        """Handed an ID, return a recipe object."""
 
398
        if recipe_table:
 
399
            print 'handing get_rec an recipe_table is deprecated'
 
400
            print 'Ignoring recipe_table handed to get_rec'
 
401
        recipe_table=self.recipe_table
 
402
        s = recipe_table.select(id=id)
 
403
        if len(s)>0:
 
404
            return recipe_table.select(id=id)[0]
 
405
        else:
 
406
            return None
 
407
 
 
408
    def add_rec (self, rdict):
 
409
        """Add a recipe based on a dictionary of properties and values."""
 
410
        self.changed=True
 
411
        t = TimeAction('rdatabase.add_rec - checking keys',3)
 
412
        if not rdict.has_key('deleted'):
 
413
            rdict['deleted']=0
 
414
        if not rdict.has_key('id'):
 
415
            rdict['id']=self.new_id()
 
416
        t.end()
 
417
        try:
 
418
            debug('Adding recipe %s'%rdict, 4)
 
419
            t = TimeAction('rdatabase.add_rec - recipe_table.append(rdict)',3)
 
420
            self.recipe_table.append(rdict)
 
421
            t.end()
 
422
            debug('Running add hooks %s'%self.add_hooks,2)
 
423
            if self.add_hooks: self.run_hooks(self.add_hooks,self.recipe_table[-1])
 
424
            return self.recipe_table[-1]
 
425
        except:
 
426
            debug("There was a problem adding recipe%s"%rdict,-1)
 
427
            raise
 
428
 
 
429
    def delete_rec (self, rec):
 
430
        """Delete recipe object rec from our database."""
 
431
        raise NotImplementedError
 
432
 
 
433
    def undoable_delete_recs (self, recs, history, make_visible=None):
 
434
        """Delete recipes by setting their 'deleted' flag to True and add to UNDO history."""
 
435
        def do_delete ():
 
436
            for rec in recs: rec.deleted = True
 
437
            if make_visible: make_visible(recs)
 
438
        def undo_delete ():
 
439
            for rec in recs: rec.deleted = False
 
440
            if make_visible: make_visible(recs)
 
441
        obj = Undo.UndoableObject(do_delete,undo_delete,history)
 
442
        obj.perform()
 
443
 
 
444
    def new_rec (self):
 
445
        """Create and return a new, empty recipe"""
 
446
        blankdict = {'id':self.new_id(),
 
447
                     'title':_('New Recipe'),
 
448
                     #'servings':'4'}
 
449
                     }
 
450
        return self.add_rec(blankdict)
 
451
 
 
452
    def new_id (self, base="r"):
 
453
        """Return a new unique ID. Possibly, we can have a base"""
 
454
        if self.top_id.has_key(base):
 
455
            start = self.top_id[base]
 
456
            n = start + 1
 
457
        else:
 
458
            n = 0
 
459
        while self.recipe_table.find(id=self.format_id(n, base)) > -1 or self.ingredients_table.find(id=self.format_id(n, base)) > -1:
 
460
            # if the ID exists, we keep incrementing
 
461
            # until we find a unique ID
 
462
            n += 1 
 
463
        # every time we're called, we increment out record.
 
464
        # This way, if party A asks for an ID and still hasn't
 
465
        # committed a recipe by the time party B asks for an ID,
 
466
        # they'll still get unique IDs.
 
467
        self.top_id[base]=n
 
468
        return self.format_id(n, base)
 
469
 
 
470
    def format_id (self, n, base="r"):
 
471
        return base+str(n)
 
472
 
 
473
    def add_ing (self, ingdict):
 
474
        """Add ingredient to ingredients_table based on ingdict and return
 
475
        ingredient object. Ingdict contains:
 
476
        id: recipe_id
 
477
        unit: unit
 
478
        item: description
 
479
        key: keyed descriptor
 
480
        alternative: not yet implemented (alternative)
 
481
        #optional: yes|no
 
482
        optional: True|False (boolean)
 
483
        position: INTEGER [position in list]
 
484
        refid: id of reference recipe. If ref is provided, everything
 
485
               else is irrelevant except for amount.
 
486
        """
 
487
        self.changed=True        
 
488
        debug('adding to ingredients_table %s'%ingdict,3)
 
489
        timer = TimeAction('rdatabase.add_ing 2',5)
 
490
        if ingdict.has_key('amount') and not ingdict['amount']: del ingdict['amount']
 
491
        self.ingredients_table.append(ingdict)
 
492
        timer.end()
 
493
        debug('running ing hooks %s'%self.add_ing_hooks,3)
 
494
        timer = TimeAction('rdatabase.add_ing 3',5)
 
495
        if self.add_ing_hooks: self.run_hooks(self.add_ing_hooks, self.ingredients_table[-1])
 
496
        timer.end()
 
497
        debug('done with ing hooks',3)
 
498
        return self.ingredients_table[-1]
 
499
 
 
500
    def undoable_modify_ing (self, ing, dic, history, make_visible=None):
 
501
        """modify ingredient object ing based on a dictionary of properties and new values.
 
502
 
 
503
        history is our undo history to be handed to Undo.UndoableObject
 
504
        make_visible is a function that will make our change (or the undo or our change) visible.
 
505
        """
 
506
        orig_dic = self.get_dict_for_obj(ing,dic.keys())
 
507
        def do_action ():
 
508
            debug('undoable_modify_ing modifying %s'%dic,2)
 
509
            self.modify_ing(ing,dic)
 
510
            if make_visible: make_visible(ing,dic)
 
511
        def undo_action ():
 
512
            debug('undoable_modify_ing unmodifying %s'%orig_dic,2)
 
513
            self.modify_ing(ing,orig_dic)
 
514
            if make_visible: make_visible(ing,orig_dic)
 
515
        obj = Undo.UndoableObject(do_action,undo_action,history)
 
516
        obj.perform()
 
517
        
 
518
    def modify_ing (self, ing, ingdict):
 
519
        """modify ing based on dictionary of properties and new values."""
 
520
        #self.delete_ing(ing)
 
521
        #return self.add_ing(ingdict)
 
522
        for k,v in ingdict.items():
 
523
            if hasattr(ing,k):
 
524
                self.changed=True
 
525
                setattr(ing,k,v)
 
526
            else:
 
527
                debug("Warning: ing has no attribute %s (attempted to set value to %s" %(k,v),0)
 
528
        return ing
 
529
 
 
530
    def replace_ings (self, ingdicts):
 
531
        """Add a new ingredients and remove old ingredient list."""
 
532
        ## we assume (hope!) all ingdicts are for the same ID
 
533
        id=ingdicts[0]['id']
 
534
        debug("Deleting ingredients for recipe with ID %s"%id,1)
 
535
        ings = self.get_ings(id)
 
536
        for i in ings:
 
537
            debug("Deleting ingredient: %s"%i.ingredient,5)
 
538
            self.delete_ing(i)
 
539
        for ingd in ingdicts:
 
540
            self.add_ing(ingd)
 
541
 
 
542
    def undoable_delete_ings (self, ings, history, make_visible=None):
 
543
        """Delete ingredients in list ings and add to our undo history."""
 
544
        def do_delete():
 
545
            for i in ings:
 
546
                i.deleted=True
 
547
            if make_visible:
 
548
                make_visible(ings)
 
549
        def undo_delete ():
 
550
            for i in ings: i.deleted=False
 
551
            if make_visible: make_visible(ings)
 
552
        obj = Undo.UndoableObject(do_delete,undo_delete,history)
 
553
        obj.perform()
 
554
 
 
555
    def delete_ing (self, ing):
 
556
        """Delete ingredient permanently."""
 
557
        raise NotImplementedError
 
558
    
 
559
class RecipeManager (RecData):
 
560
    
 
561
    def __init__ (self):
 
562
        debug('recipeManager.__init__()',3)
 
563
        RecData.__init__(self)
 
564
        self.km = keymanager.KeyManager(rm=self)
 
565
        
 
566
    def key_search (self, ing):
 
567
        """Handed a string, we search for keys that could match
 
568
        the ingredient."""
 
569
        result=self.km.look_for_key(ing)
 
570
        if type(result)==type(""):
 
571
            return [result]
 
572
        elif type(result)==type([]):
 
573
            # look_for contains an alist of sorts... we just want the first
 
574
            # item of every cell.
 
575
            if len(result)>0 and result[0][1]>0.8:
 
576
                return map(lambda a: a[0],result)
 
577
            else:
 
578
                ## otherwise, we make a mad attempt to guess!
 
579
                k=self.km.generate_key(ing)
 
580
                l = [k]
 
581
                l.extend(map(lambda a: a[0],result))
 
582
                return l
 
583
        else:
 
584
            return None
 
585
            
 
586
    def ingredient_parser (self, s, conv=None, get_key=True):
 
587
        """Handed a string, we hand back a dictionary (sans recipe ID)"""
 
588
        debug('ingredient_parser handed: %s'%s,0)
 
589
        s = unicode(s) # convert to unicode so our ING MATCHER works properly
 
590
        s=s.strip("\n\t #*+-")
 
591
        debug('ingredient_parser handed: "%s"'%s,1)
 
592
        m=convert.ING_MATCHER.match(s)
 
593
        if m:
 
594
            debug('ingredient parser successfully parsed %s'%s,1)
 
595
            d={}
 
596
            g=m.groups()
 
597
            a,u,i=(g[convert.ING_MATCHER_AMT_GROUP],
 
598
                   g[convert.ING_MATCHER_UNIT_GROUP],
 
599
                   g[convert.ING_MATCHER_ITEM_GROUP])
 
600
            if a:
 
601
                asplit = convert.RANGE_MATCHER.split(a)
 
602
                if len(asplit)==2:
 
603
                    d['amount']=convert.frac_to_float(asplit[0].strip())
 
604
                    d['rangeamount']=convert.frac_to_float(asplit[1].strip())
 
605
                else:
 
606
                    d['amount']=convert.frac_to_float(a.strip())
 
607
            if u:
 
608
                if conv and conv.unit_dict.has_key(u.strip()):
 
609
                    d['unit']=conv.unit_dict[u.strip()]
 
610
                else:
 
611
                    d['unit']=u.strip()
 
612
            if i:
 
613
                optmatch = re.search('\s+\(?[Oo]ptional\)?',i)
 
614
                if optmatch:
 
615
                    d['optional']=True
 
616
                    i = i[0:optmatch.start()] + i[optmatch.end():]
 
617
                d['item']=i.strip()
 
618
                if get_key: d['ingkey']=self.km.get_key(i.strip())
 
619
            debug('ingredient_parser returning: %s'%d,0)
 
620
            return d
 
621
        else:
 
622
            debug("Unable to parse %s"%s,0)
 
623
            return None
 
624
 
 
625
    def ing_search (self, ing, keyed=None, recipe_table=None, use_regexp=True, exact=False):
 
626
        """Search for an ingredient."""
 
627
        raise NotImplementedError
 
628
    
 
629
    def ings_search (self, ings, keyed=None, recipe_table=None, use_regexp=True, exact=False):
 
630
        """Search for multiple ingredients."""
 
631
        raise NotImplementedError
 
632
 
 
633
    def clear_remembered_optional_ings (self, recipe=None):
 
634
        """Clear our memories of optional ingredient defaults.
 
635
 
 
636
        If handed a recipe, we clear only for the recipe we've been
 
637
        given.
 
638
 
 
639
        Otherwise, we clear *all* recipes.
 
640
        """
 
641
        if recipe:
 
642
            vw = self.get_ings(recipe)
 
643
        else:
 
644
            vw = self.ingredients_table
 
645
        # this is ugly...
 
646
        vw1 = vw.select(shopoptional=1)
 
647
        vw2 = vw.select(shopoptional=2)
 
648
        for v in vw1,vw2:
 
649
            for i in v: self.modify_ing(i,{'shopoptional':0})
 
650
 
 
651
class mkConverter(convert.converter):
 
652
    def __init__ (self, db):
 
653
        self.db = db
 
654
        convert.converter.__init__(self)
 
655
    ## still need to finish this class and then
 
656
    ## replace calls to convert.converter with
 
657
    ## calls to rmetakit.mkConverter
 
658
 
 
659
    def create_conv_table (self):
 
660
        self.conv_table = dbDic('ckey','value',self.db.convtable_table, self.db,
 
661
                                pickle_key=True)
 
662
        for k,v in defaults.CONVERTER_TABLE.items():
 
663
            if not self.conv_table.has_key(k):
 
664
                self.conv_table[k]=v
 
665
 
 
666
    def create_density_table (self):
 
667
        self.density_table = dbDic('dkey','value',
 
668
                                   self.db.density_table,self.db)
 
669
        for k,v in defaults.DENSITY_TABLE.items():
 
670
            if not self.density_table.has_key(k):
 
671
                self.density_table[k]=v
 
672
 
 
673
    def create_cross_unit_table (self):
 
674
        self.cross_unit_table=dbDic('cukey','value',self.db.crossunitdict_table,self.db)
 
675
        for k,v in defaults.CROSS_UNIT_TABLE:
 
676
            if not self.cross_unit_table.has_key(k):
 
677
                self.cross_unit_table[k]=v
 
678
 
 
679
    def create_unit_dict (self):
 
680
        self.units = defaults.UNITS
 
681
        self.unit_dict=dbDic('ukey','value',self.db.unitdict_table,self.db)
 
682
        for itm in self.units:
 
683
            key = itm[0]
 
684
            variations = itm[1]
 
685
            self.unit_dict[key] = key
 
686
            for v in variations:
 
687
                self.unit_dict[v] = key
 
688
                
 
689
class dbDic:
 
690
    def __init__ (self, keyprop, valprop, view, db, pickle_key=False):
 
691
        """Create a dictionary interface to a database table."""
 
692
        self.pickle_key = pickle_key
 
693
        self.vw = view
 
694
        self.kp = keyprop
 
695
        self.vp = valprop
 
696
        self.db = db
 
697
        self.just_got = {}
 
698
 
 
699
    def has_key (self, k):
 
700
        try:
 
701
            self.just_got = {k:self.__getitem__(k)}
 
702
            return True
 
703
        except:
 
704
            try:
 
705
                self.__getitem__(k)
 
706
                return True
 
707
            except:
 
708
                return False
 
709
        
 
710
    def __setitem__ (self, k, v):
 
711
        if self.pickle_key:
 
712
            k=pickle.dumps(k)
 
713
        row = self.vw.select(**{self.kp:k})
 
714
        if len(row)>0:
 
715
            setattr(row[0],self.vp,pickle.dumps(v))
 
716
        else:
 
717
            self.vw.append({self.kp:k,self.vp:pickle.dumps(v)})
 
718
        self.db.changed=True
 
719
        return v
 
720
    
 
721
    def __getitem__ (self, k):
 
722
        if self.just_got.has_key(k): return self.just_got[k]
 
723
        if self.pickle_key:
 
724
            k=pickle.dumps(k)
 
725
        t=TimeAction('dbdict getting from db',5)
 
726
        v = getattr(self.vw.select(**{self.kp:k})[0],self.vp)        
 
727
        t.end()
 
728
        if v:
 
729
            try:
 
730
                return pickle.loads(v)
 
731
            except:
 
732
                print "Problem unpickling ",v                
 
733
                raise
 
734
        else:
 
735
            return None
 
736
    
 
737
    def __repr__ (self):
 
738
        str = "<dbDic> {"
 
739
        for i in self.vw:
 
740
            if self.pickle_key:
 
741
                str += "%s"%pickle.loads(getattr(i,self.kp))
 
742
            else:
 
743
                str += getattr(i,self.kp)
 
744
            str += ":"
 
745
            str += "%s"%pickle.loads(getattr(i,self.vp))
 
746
            str += ", "
 
747
        str += "}"
 
748
        return str
 
749
 
 
750
    def keys (self):
 
751
        ret = []
 
752
        for i in self.vw:
 
753
            ret.append(getattr(i,self.kp))
 
754
        return ret
 
755
 
 
756
    def values (self):
 
757
        ret = []
 
758
        for i in self.vw:
 
759
            val = getattr(i,self.vp)
 
760
            if val: val = pickle.loads(val)
 
761
            ret.append(val)
 
762
        return ret
 
763
 
 
764
    def items (self):
 
765
        ret = []
 
766
        for i in self.vw:
 
767
            key = getattr(i,self.kp)
 
768
            val = getattr(i,self.vp)
 
769
            if key and self.pickle_key:
 
770
                try:
 
771
                    key = pickle.loads(key)
 
772
                except:
 
773
                    print 'Problem unpickling key ',key
 
774
                    raise
 
775
            if val:
 
776
                try:
 
777
                    val = pickle.loads(val)
 
778
                except:
 
779
                    print 'Problem unpickling value ',val, ' for key ',key
 
780
                    print """Fearlessly, stupidly pushing forward!
 
781
                    (This may help us with corrupt data, but this
 
782
                    shouldn't be a normal part of our business).
 
783
                    """
 
784
            ret.append((key,val))
 
785
        return ret