refactor validation
[tiramisu.git] / tiramisu / storage / sqlalchemy / option.py
1 # -*- coding: utf-8 -*-
2 ""
3 # Copyright (C) 2014 Team tiramisu (see AUTHORS for all contributors)
4 #
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 2 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18 #
19 # ____________________________________________________________
20 from tiramisu.i18n import _
21 from tiramisu.setting import groups, undefined
22 from tiramisu.error import ConfigError
23 from .util import SqlAlchemyBase
24 import util
25
26 from sqlalchemy import not_, or_, and_, inspect
27 from sqlalchemy.ext.declarative import declared_attr
28 from sqlalchemy.ext.associationproxy import association_proxy
29 from sqlalchemy import Column, Integer, String, Boolean, PickleType, \
30     ForeignKey, Table
31 from sqlalchemy.orm import relationship, backref
32 from sqlalchemy.orm.collections import attribute_mapped_collection
33
34 from itertools import chain
35
36
37 def load_requires(collection_type, proxy):
38     def getter(obj):
39         if obj is None:
40             return None
41         ret = []
42         requires = getattr(obj, proxy.value_attr)
43         for require in requires:
44             option = util.session.query(_Base).filter_by(id=require.option).first()
45             ret.append(tuple([option, require.expected, require.action, require.inverse, require.transitive, require.same_action]))
46         return tuple(ret)
47
48     def setter(obj, value):
49         setattr(obj, proxy.value_attr, value)
50     return getter, setter
51
52
53 class _Require(SqlAlchemyBase):
54     __tablename__ = "require"
55     id = Column(Integer, primary_key=True)
56     requires_id = Column(Integer, ForeignKey("baseoption.id"), nullable=False)
57     requires = relationship('_RequireOption')
58
59     def __init__(self, requires):
60         for require in requires:
61             self.requires.append(_RequireOption(require))
62
63
64 class _RequireOption(SqlAlchemyBase):
65     __tablename__ = 'requireoption'
66     id = Column(Integer, primary_key=True)
67     require_id = Column(Integer, ForeignKey("require.id"), nullable=False)
68     option = Column(Integer, nullable=False)
69     _expected = relationship("_RequireExpected", collection_class=list,
70                              cascade="all, delete-orphan")
71     expected = association_proxy("_expected", "expected")
72     #expected = Column(String)
73     action = Column(String, nullable=False)
74     inverse = Column(Boolean, default=False)
75     transitive = Column(Boolean, default=True)
76     same_action = Column(Boolean, default=True)
77
78     def __init__(self, values):
79         option, expected, action, inverse, transitive, same_action = values
80         self.option = option.id
81         self.expected = expected
82         self.action = action
83         self.inverse = inverse
84         self.transitive = transitive
85         self.same_action = same_action
86
87
88 class _RequireExpected(SqlAlchemyBase):
89     __tablename__ = 'expected'
90     id = Column(Integer, primary_key=True)
91     require = Column(Integer, ForeignKey('requireoption.id'), nullable=False)
92     expected = Column(PickleType)
93
94     def __init__(self, expected):
95         #FIXME ne pas creer plusieurs fois la meme _expected_
96         #FIXME pareil avec calc_properties
97         self.expected = expected
98
99
100 class _CalcProperties(SqlAlchemyBase):
101     __tablename__ = 'calcproperty'
102     id = Column(Integer, primary_key=True)
103     require = Column(Integer, ForeignKey('baseoption.id'), nullable=False)
104     name = Column(PickleType)
105
106     def __init__(self, name):
107         #FIXME ne pas creer plusieurs fois la meme _expected_
108         #FIXME pareil avec calc_properties
109         self.name = name
110
111
112 #____________________________________________________________
113 #
114 # properties
115 class _PropertyOption(SqlAlchemyBase):
116     __tablename__ = 'propertyoption'
117     id = Column(Integer, primary_key=True)
118     option = Column(Integer, ForeignKey('baseoption.id'), nullable=False)
119     name = Column(String)
120
121     def __init__(self, name):
122         self.name = name
123
124
125 #____________________________________________________________
126 #
127 # information
128 class _Information(SqlAlchemyBase):
129     __tablename__ = 'information'
130     id = Column(Integer, primary_key=True)
131     option = Column(Integer, ForeignKey('baseoption.id'), nullable=False)
132     key = Column(String)
133     value = Column(PickleType)
134
135     def __init__(self, key, value):
136         self.key = key
137         self.value = value
138
139
140 #____________________________________________________________
141 #
142 # callback
143 def load_callback_parm(collection_type, proxy):
144     def getter(obj):
145         if obj is None:
146             return None
147         ret = []
148         requires = getattr(obj, proxy.value_attr)
149         for require in requires:
150             if require.value is not None:
151                 ret.append(require.value)
152             else:
153                 option = util.session.query(_Base).filter_by(id=require.option).first()
154                 ret.append((option, require.force_permissive))
155         return tuple(ret)
156
157     def setter(obj, value):
158         setattr(obj, proxy.value_attr, value)
159     return getter, setter
160
161
162 class _CallbackParamOption(SqlAlchemyBase):
163     __tablename__ = 'callback_param_option'
164     id = Column(Integer, primary_key=True)
165     callback_param = Column(Integer, ForeignKey('callback_param.id'))
166     option = Column(Integer)
167     force_permissive = Column(Boolean)
168     value = Column(PickleType)
169
170     def __init__(self, option=undefined, force_permissive=undefined,  value=undefined):
171         if value is not undefined:
172             self.value = value
173         elif option is not undefined:
174             self.option = option.id
175             self.force_permissive = force_permissive
176
177
178 class _CallbackParam(SqlAlchemyBase):
179     __tablename__ = 'callback_param'
180     id = Column(Integer, primary_key=True)
181     callback = Column(Integer, ForeignKey('baseoption.id'))
182     key = Column(String)
183     params = relationship('_CallbackParamOption')
184
185     def __init__(self, key, params):
186         self.key = key
187         for param in params:
188             if isinstance(param, tuple):
189                 if param == (None,):
190                     self.params.append(_CallbackParamOption())
191                 else:
192                     self.params.append(_CallbackParamOption(option=param[0],
193                                                             force_permissive=param[1]))
194             else:
195                 self.params.append(_CallbackParamOption(value=param))
196
197
198 #____________________________________________________________
199 #
200 # consistency
201 consistency_table = Table('consistencyopt', SqlAlchemyBase.metadata,
202                           Column('id', Integer, primary_key=True),
203                           Column('left_id', Integer, ForeignKey('consistency.id')),
204                           Column('right_id', Integer, ForeignKey('baseoption.id'))
205                           )
206
207
208 class _Consistency(SqlAlchemyBase):
209     __tablename__ = 'consistency'
210     id = Column(Integer, primary_key=True)
211     func = Column(PickleType)
212     params = Column(PickleType)
213
214     def __init__(self, func, all_cons_opts, params):
215         self.func = func
216         for option in all_cons_opts:
217             option._consistencies.append(self)
218             print type(option._consistencies)
219         self.params = params
220
221
222 class _Parent(SqlAlchemyBase):
223     __tablename__ = 'parent'
224     id = Column(Integer, primary_key=True)
225     child_id = Column(Integer)
226     child_name = Column(String)
227     parent_id = Column(Integer)
228
229     def __init__(self, parent, child):
230         self.parent_id = parent.id
231         self.child_id = child.id
232         self.child_name = child._name
233
234
235 #____________________________________________________________
236 #
237 # Base
238 class _Base(SqlAlchemyBase):
239     __tablename__ = 'baseoption'
240     id = Column(Integer, primary_key=True)
241     _name = Column(String)
242     #FIXME not autoload
243     _infos = relationship("_Information",
244                           collection_class=attribute_mapped_collection('key'),
245                           cascade="all, delete-orphan")
246     _informations = association_proxy("_infos", "value")
247     _default = Column(PickleType)
248     _default_multi = Column(PickleType)
249     _subdyn = Column(Integer)
250     _dyn = Column(String)
251     _opt = Column(Integer)
252     _choice_values = Column(PickleType)
253     _cho_params = relationship('_CallbackParam',
254                                collection_class=
255                                attribute_mapped_collection('key'))
256     _choice_params = association_proxy("_cho_params", "params",
257                                        getset_factory=load_callback_parm)
258     _reqs = relationship("_Require", collection_class=list)
259     _requires = association_proxy("_reqs", "requires", getset_factory=load_requires)
260     _multi = Column(Integer)
261     ######
262     _callback = Column(PickleType)
263     _call_params = relationship('_CallbackParam',
264                                 collection_class=
265                                 attribute_mapped_collection('key'))
266     _callback_params = association_proxy("_call_params", "params",
267                                          getset_factory=load_callback_parm)
268     _validator = Column(PickleType)
269     _val_params = relationship('_CallbackParam',
270                                collection_class=
271                                attribute_mapped_collection('key'))
272     _validator_params = association_proxy("_val_params", "params",
273                                           getset_factory=load_callback_parm)
274     ######
275     #FIXME not autoload
276     _props = relationship("_PropertyOption", collection_class=set)
277     _properties = association_proxy("_props", "name")
278     _calc_props = relationship("_CalcProperties", collection_class=set)
279     _calc_properties = association_proxy("_calc_props", "name")
280     _warnings_only = Column(Boolean)
281     _readonly = Column(Boolean, default=False)
282     _consistencies = relationship('_Consistency', secondary=consistency_table,
283                                   backref=backref('options',
284                                                   enable_typechecks=False))
285     _type = Column(String(50))
286     _stated = Column(Boolean)
287     __mapper_args__ = {
288         'polymorphic_identity': 'option',
289         'polymorphic_on': _type
290     }
291     _extra = Column(PickleType)
292     #FIXME devrait etre une table
293     _group_type = Column(String)
294     _is_build_cache = Column(Boolean, default=False)
295
296     #def __init__(self):
297     def __init__(self, name, multi, warnings_only, doc, extra, calc_properties,
298                  requires, properties, opt=undefined):
299         util.session.add(self)
300         self._name = name
301         if multi is not undefined:
302             self._multi = multi
303         if warnings_only is not undefined:
304             self._warnings_only = warnings_only
305         if doc is not undefined:
306             self._informations = {'doc': doc}
307         if opt is not undefined:
308             self._opt = opt.id
309         if extra is not undefined:
310             self._extra = extra
311         if calc_properties is not undefined:
312             self._calc_properties = calc_properties
313         if requires is not undefined:
314             self._requires = requires
315         if properties is not undefined:
316             self._properties = properties
317
318     def commit(self):
319         util.session.commit()
320
321     def _add_consistency(self, func, all_cons_opts, params):
322         _Consistency(func, all_cons_opts, params)
323
324     def _set_default_values(self, default, default_multi):
325         self._default = default
326         if self.impl_is_multi() and default_multi is not None:
327             self._validate(default_multi)
328             self._default_multi = default_multi
329
330     def _get_consistencies(self):
331         return [(consistency.func, consistency.options, consistency.params)
332                 for consistency in self._consistencies]
333
334     def _get_id(self):
335         return self.id
336
337     def impl_get_callback(self):
338         ret = self._callback
339         if ret is None:
340             return (None, {})
341         return ret, self._callback_params
342
343     def impl_get_validator(self):
344         ret = self._validator
345         if ret is None:
346             return (None, {})
347         return ret, self._validator_params
348
349     def _impl_getsubdyn(self):
350         return util.session.query(_Base).filter_by(id=self._subdyn).first()
351
352     def _impl_getopt(self):
353         return util.session.query(_Base).filter_by(id=self._opt).first()
354
355     def impl_getname(self):
356         return self._name
357
358     def impl_getrequires(self):
359         return self._requires
360
361     def impl_getdefault(self):
362         ret = self._default
363         if self.impl_is_multi():
364             if ret is None:
365                 return []
366             return list(ret)
367         return ret
368
369     def impl_getdefault_multi(self):
370         if self.impl_is_multi():
371             return self._default_multi
372
373     def _get_extra(self, key):
374         return self._extra[key]
375
376     def _impl_setopt(self, opt):
377         self._opt = opt.id
378
379     def _impl_setsubdyn(self, subdyn):
380         self._subdyn = subdyn.id
381         self.commit()
382
383     def _set_readonly(self):
384         self._readonly = True
385
386     def _set_callback(self, callback, callback_params):
387         self._callback = callback
388         if callback_params is not None:
389             self._callback_params = callback_params
390
391     def _set_validator(self, validator, validator_params):
392         self._validator = validator
393         if validator_params is not None:
394             self._validator_params = validator_params
395
396     def impl_is_readonly(self):
397         try:
398             return self._readonly
399         except AttributeError:
400             return False
401
402     def impl_is_multi(self):
403         return self._multi == 0 or self._multi == 2
404
405     def impl_is_submulti(self):
406         return self._multi == 2
407
408     def _is_warnings_only(self):
409         return self._warnings_only
410
411     def impl_get_calc_properties(self):
412         try:
413             return self._calc_properties
414         except AttributeError:
415             return frozenset()
416
417     # information
418     def impl_set_information(self, key, value):
419         self._informations[key] = value
420
421     def impl_get_information(self, key, default=undefined):
422         """retrieves one information's item
423
424         :param key: the item string (ex: "help")
425         """
426         if default is not undefined:
427             return self._informations.get(key, default)
428         try:
429             return self._informations[key]
430         except KeyError:  # pragma: optional cover
431             raise ValueError(_("information's item not found: {0}").format(
432                 key))
433
434     def _impl_getattributes(self):
435         slots = set()
436         mapper = inspect(self)
437         for column in mapper.attrs:
438                 slots.add(column.key)
439         return slots
440
441
442 class Cache(SqlAlchemyBase):
443     __tablename__ = 'cache'
444     id = Column(Integer, primary_key=True)
445     path = Column(String, nullable=False, index=True)
446     descr = Column(Integer, nullable=False, index=True)
447     parent = Column(Integer, nullable=False, index=True)
448     option = Column(Integer, nullable=False, index=True)
449     opt_type = Column(String, nullable=False, index=True)
450     is_subdyn = Column(Boolean, nullable=False, index=True)
451     subdyn_path = Column(String)
452
453     def __init__(self, descr, parent, option, path, subdyn_path):
454         #context
455         self.descr = descr.id
456         self.parent = parent.id
457         self.option = option.id
458         self.path = path
459         self.opt_type = option.__class__.__name__
460         if subdyn_path:
461             self.is_subdyn = True
462             self.subdyn_path = subdyn_path
463         else:
464             self.is_subdyn = False
465             self.subdyn_path = None
466
467
468 class StorageOptionDescription(object):
469     def impl_already_build_caches(self):
470         return self._is_build_cache
471
472     def impl_get_opt_by_path(self, path):
473         ret = util.session.query(Cache).filter_by(descr=self.id, path=path).first()
474         if ret is None:
475             raise AttributeError(_('no option for path {0}').format(path))
476         return util.session.query(_Base).filter_by(id=ret.option).first()
477
478     def impl_get_path_by_opt(self, opt):
479         ret = util.session.query(Cache).filter_by(descr=self.id,
480                                                   option=opt.id).first()
481         if ret is None:
482             raise AttributeError(_('no option {0} found').format(opt))
483         return ret.path
484
485     def impl_get_group_type(self):
486         return getattr(groups, self._group_type)
487
488     def impl_build_cache_option(self, descr=None, _currpath=None,
489                                 subdyn_path=None):
490         if descr is None:
491             save = True
492             descr = self
493             _currpath = []
494         else:
495             save = False
496         for option in self._impl_getchildren(dyn=False):
497             attr = option.impl_getname()
498             if isinstance(option, StorageOptionDescription):
499                 sub = subdyn_path
500                 if option.impl_is_dynoptiondescription():
501                     sub = '.'.join(_currpath)
502                 util.session.add(Cache(descr, self, option,
503                                        str('.'.join(_currpath + [attr])),
504                                        sub))
505                 _currpath.append(attr)
506                 option.impl_build_cache_option(descr,
507                                                _currpath,
508                                                sub)
509                 _currpath.pop()
510             else:
511                 if subdyn_path:
512                     subdyn_path = '.'.join(_currpath)
513                 util.session.add(Cache(descr, self, option,
514                                        str('.'.join(_currpath + [attr])),
515                                        subdyn_path))
516         if save:
517             self._is_build_cache = True
518             util.session.commit()
519
520     def impl_get_options_paths(self, bytype, byname, _subpath, only_first,
521                                context):
522         def _build_ret_opt(opt, option, suffix, name):
523             subdyn_path = opt.subdyn_path
524             dynpaths = opt.path[len(subdyn_path):].split('.')
525
526             path = subdyn_path
527             dot = False
528             for dynpath in dynpaths:
529                 if dot:
530                     path += '.'
531                 path += dynpath + suffix
532                 dot = True
533             _opt = option._impl_to_dyn(name + suffix, path)
534             return (path, _opt)
535
536         sqlquery = util.session.query(Cache).filter_by(descr=self.id)
537         if bytype is None:
538             sqlquery = sqlquery.filter(and_(not_(
539                 Cache.opt_type == 'OptionDescription'),
540                 not_(Cache.opt_type == 'DynOptionDescription')))
541         else:
542             sqlquery = sqlquery.filter_by(opt_type=bytype.__name__)
543
544         query = ''
545         or_query = ''
546         if _subpath is not None:
547             query += _subpath + '.%'
548         #if byname is not None:
549         #    or_query = query + byname
550         #    query += '%.' + byname
551         if query != '':
552             filter_query = Cache.path.like(query)
553             if or_query != '':
554                 filter_query = or_(Cache.path == or_query, filter_query)
555             sqlquery = sqlquery.filter(filter_query)
556         #if only_first:
557         #    opt = sqlquery.first()
558         #    if opt is None:
559         #        return tuple()
560         #    option = util.session.query(_Base).filter_by(id=opt.option).first()
561         #    return ((opt.path, option),)
562         #else:
563         ret = []
564         for opt in sqlquery.all():
565             option = util.session.query(_Base).filter_by(id=opt.option).first()
566             if opt.is_subdyn:
567                 name = option.impl_getname()
568                 if byname is not None:
569                     if byname.startswith(name):
570                         found = False
571                         dynoption = option._impl_getsubdyn()
572                         for suffix in dynoption._impl_get_suffixes(
573                                 context):
574                             if byname == name + suffix:
575                                 found = True
576                                 break
577                         if not found:
578                             continue
579                         ret_opt = _build_ret_opt(opt, option, suffix, name)
580                     else:
581                         ret_opt = _build_ret_opt(opt, option, suffix, name)
582                 else:
583                     if not only_first:
584                         ret_opt = []
585                     dynoption = option._impl_getsubdyn()
586                     for suffix in dynoption._impl_get_suffixes(context):
587                         val = _build_ret_opt(opt, option, suffix, name)
588                         if only_first:
589                             ret_opt = val
590                         else:
591                             ret_opt.append(val)
592             else:
593                 if byname is not None and byname != option.impl_getname():
594                     continue
595                 ret_opt = (opt.path, option)
596             if only_first:
597                 return ret_opt
598             if isinstance(ret_opt, list):
599                 if ret_opt != []:
600                     ret.extend(ret_opt)
601             else:
602                 ret.append(ret_opt)
603         return ret
604
605     def _add_children(self, child_names, children):
606         for child in children:
607             util.session.add(_Parent(self, child))
608
609     def _impl_st_getchildren(self, context, only_dyn=False):
610         if only_dyn is False or context is undefined:
611             for child in util.session.query(_Parent).filter_by(
612                     parent_id=self.id).all():
613                 yield(util.session.query(_Base).filter_by(id=child.child_id
614                                                           ).first())
615         else:
616             descr = context.cfgimpl_get_description().id
617             for child in util.session.query(Cache).filter_by(descr=descr,
618                                                              parent=self.id
619                                                              ).all():
620                 yield(util.session.query(_Base).filter_by(id=child.option).first())
621
622     def _getattr(self, name, suffix=undefined, context=undefined, dyn=True):
623         error = False
624         if suffix is not undefined:
625             try:
626                 if undefined in [suffix, context]:  # pragma: optional cover
627                     raise ConfigError(_("suffix and context needed if "
628                                         "it's a dyn option"))
629                 if name.endswith(suffix):
630                     oname = name[:-len(suffix)]
631                     #child = self._children[1][self._children[0].index(oname)]
632                     child = util.session.query(_Parent).filter_by(
633                         parent_id=self.id, child_name=oname).first()
634                     if child is None:
635                         error = True
636                     else:
637                         opt = util.session.query(_Base).filter_by(
638                             id=child.child_id).first()
639                         return self._impl_get_dynchild(opt, suffix)
640                 else:
641                     error = True
642             except ValueError:  # pragma: optional cover
643                 error = True
644         else:
645             child = util.session.query(_Parent).filter_by(parent_id=self.id,
646                                                           child_name=name
647                                                           ).first()
648             if child is None:
649                 child = self._impl_search_dynchild(name, context=context)
650                 if child != []:
651                     return child
652                 error = True
653             if error is False:
654                 return util.session.query(_Base).filter_by(id=child.child_id
655                                                            ).first()
656         if error:
657             raise AttributeError(_('unknown Option {0} in OptionDescription {1}'
658                                  '').format(name, self.impl_getname()))
659
660     def _get_force_store_value(self):
661         #only option in current tree
662         current_ids = tuple(chain(*util.session.query(Cache.option).filter_by(
663             descr=self.id).all()))
664         for prop in util.session.query(_PropertyOption).filter(
665                 _PropertyOption.option.in_(current_ids),
666                 _PropertyOption.name == 'force_store_value').all():
667             opt = util.session.query(_Base).filter_by(id=prop.option).first()
668             path = self.impl_get_path_by_opt(opt)
669             yield (opt, path)
670
671
672 class StorageBase(_Base):
673     @declared_attr
674     def __mapper_args__(self):
675         return {'polymorphic_identity': self.__name__.lower()}