Add MultiValueMixin and ValueMixin and remove Propery

issue2349
review23351002
This commit is contained in:
Cédric Krier committed 2017-04-01 00:24:55 +02:00
1 parent aa9c9cc9e7
commit a908e8cc74
25 files changed
+447 -806

No files matched your search

+2
View File
@@ -1,3 +1,5 @@
* Remove Property field
* Add MultiValueMixin and ValueMixin
* Use sql type in column creation
* Use generic SQL type in field and let backend determine the SQLType
* Add filter to xxx2Many fields
+15 -19
View File
@@ -819,29 +819,25 @@ Instance methods:
:class:`~trytond.model.Model` instance of the field, `name` is the name of
the field, `clause` is a clause of :ref:`domain <topics-domain>`.
Property
--------
MultiValue
----------
.. class:: Property(field)
.. class:: MultiValue(field)
A property field that is like a :class:`Function` field but with predifined
:attr:`~Function.getter`, :attr:`~Function.setter` and
:attr:`~Function.searcher` that use the :class:`~trytond.model.ModelSQL`
`ir.property` to store values.
A multivalue field that is like a :class:`Function` field but with predefined
:attr:`~Function.getter` and :attr:`~Function.setter` that use the
:class:`~trytond.model.MultiValueMixin` for stored values.
Instance methods:
.. warning::
The :meth:`~trytond.model.MultiValueMixin.get_multivalue` and
:meth:`~trytond.model.MultiValueMixin.set_multivalue` should be prefered
over the descriptors of the field.
..
.. method:: Property.get(ids, model, name[, values])
Same as :meth:`Function.get`.
.. method:: Property.set(ids, model, name, value)
Same as :meth:`Function.set`.
.. method:: Property.search(model, name, clause)
Same as :meth:`Function.search`.
.. warning::
The :ref:`default <topics-fields_default_value>` method of the field must
accept pattern as keyword argument.
..
Dict
----
+57 -2
View File
@@ -647,9 +647,10 @@ record is equal or not defined.
Instance methods:
.. method:: MatchMixin.match(pattern)
.. method:: MatchMixin.match(pattern[, match_none])
Return if the instance match the pattern
Return if the instance match the pattern. If `match_none` is set `None`
value of the instance will be compared.
==========
UnionMixin
@@ -697,6 +698,60 @@ the field to be created and its default values is `sequence`. field_label
defines the label which will be used by the field and defaults to `Sequence`.
Order specifies the order direction and defaults to `ASC NULLS FIRST`.
===============
MultiValueMixin
===============
.. class:: MultiValueMixin
A mixin_ for :class:`Model` to help having
:class:`trytond.model.fields.MultiValue` fields with multi-values on a
:class:`ValueMixin`. The values are stored by creating one record per pattern.
The patterns are the same as those on :class:`MatchMixin`.
Class methods:
.. classmethod:: MultiValueMixin.multivalue_model(field)
Return the :class:`ValueMixin` on which the values are stored for the
field name. The default is class name suffixed by the field name.
.. classmethod:: MultiValueMixin.setter_multivalue(records, name, value, \*\*pattern)
The setter method for the :class:`trytond.model.fields.Function` fields.
Instance methods:
.. method:: MultiValueMixin.multivalue_records(field)
Return the list of all :class:`ValueMixin` records linked to the instance.
By default, it returns the value of the first found
:class:`trytond.model.fields.One2Many` linked to the multivalue model or
all the records of this one.
.. method:: MultiValueMixin.multivalue_record(field, \*\*pattern)
Return a new record of :class:`ValueMixin` linked to the instance.
.. method:: MultiValueMixin.get_multivalue(name, \*\*pattern)
Return the value of the field `name` for the pattern.
.. method:: MultiValueMixin.set_multivalue(name, value, \*\*pattern)
Store the value of the field `name` for the pattern.
.. warning::
To customize the pattern, both methods must be override the same way.
..
==========
ValueMixin
==========
.. class:: ValueMixin
A mixin_ to store the values of :class:`MultiValueMixin`.
.. _mixin: http://en.wikipedia.org/wiki/Mixin
.. _JSON: http://en.wikipedia.org/wiki/Json
-2
View File
@@ -7,7 +7,6 @@ from .sequence import *
from .ui.menu import *
from .ui.view import *
from .ui.icon import *
from .property import *
from .action import *
from .model import *
from .attachment import *
@@ -45,7 +44,6 @@ def register():
ViewTreeState,
ViewSearch,
Icon,
Property,
Action,
ActionKeyword,
ActionReport,
-28
View File
@@ -77,8 +77,6 @@ class Model(ModelSQL, ModelView):
@classmethod
def register(cls, model, module_name):
pool = Pool()
Property = pool.get('ir.property')
cursor = Transaction().connection.cursor()
ir_model = cls.__table__()
@@ -97,7 +95,6 @@ class Model(ModelSQL, ModelView):
ir_model.module],
[[model.__name__, model._get_name(), model.__doc__,
module_name]]))
Property._models_get_cache.clear()
cursor.execute(*ir_model.select(ir_model.id,
where=ir_model.model == model.__name__))
(model_id,) = cursor.fetchone()
@@ -124,31 +121,6 @@ class Model(ModelSQL, ModelView):
return [name for name, model in Pool().iterobject()
if getattr(model, '_history', False)]
@classmethod
def create(cls, vlist):
pool = Pool()
Property = pool.get('ir.property')
res = super(Model, cls).create(vlist)
# Restart the cache of models_get
Property._models_get_cache.clear()
return res
@classmethod
def write(cls, models, values, *args):
pool = Pool()
Property = pool.get('ir.property')
super(Model, cls).write(models, values, *args)
# Restart the cache of models_get
Property._models_get_cache.clear()
@classmethod
def delete(cls, models):
pool = Pool()
Property = pool.get('ir.property')
super(Model, cls).delete(models)
# Restart the cache of models_get
Property._models_get_cache.clear()
@classmethod
def global_search(cls, text, limit, menu='ir.ui.menu'):
"""
-173
View File
@@ -1,173 +0,0 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from decimal import Decimal
from ..model import ModelView, ModelSQL, fields
from ..transaction import Transaction
from ..cache import Cache
from ..pool import Pool
__all__ = [
'Property',
]
_CAST = {
'numeric': Decimal,
'integer': int,
'float': float,
}
class Property(ModelSQL, ModelView):
"Property"
__name__ = 'ir.property'
_rec_name = 'field'
value = fields.Reference('Value', selection='models_get')
res = fields.Reference('Resource', selection='models_get', select=True)
field = fields.Many2One('ir.model.field', 'Field',
ondelete='CASCADE', required=True, select=True)
_models_get_cache = Cache('ir_property.models_get', context=False)
@classmethod
def models_get(cls):
pool = Pool()
Model = pool.get('ir.model')
models = cls._models_get_cache.get(None)
if models:
return models
cursor = Transaction().connection.cursor()
model = Model.__table__()
cursor.execute(*model.select(model.model, model.name,
order_by=model.name.asc))
models = cursor.fetchall() + [('', '')]
cls._models_get_cache.set(None, models)
return models
@classmethod
def get(cls, names, model, res_ids=None):
"""
Return named property values for each res_ids of model
"""
pool = Pool()
ModelAccess = pool.get('ir.model.access')
res = {}
ModelAccess.check(model, 'read')
names_list = True
if not isinstance(names, list):
names_list = False
names = [names]
if res_ids is None:
res_ids = []
properties = cls.search([
('field.name', 'in', names),
['OR',
('res', '=', None),
('res', 'in', ['%s,%s' % (model, x) for x in res_ids]),
],
], order=[])
default_vals = dict((x, None) for x in names)
for property_ in (x for x in properties if not x.res):
value = property_.value
val = None
if value is not None:
if not isinstance(value, basestring):
val = int(value)
else:
if property_.field.ttype in _CAST:
cast = _CAST[property_.field.ttype]
val = cast(value.split(',')[1])
elif property_.field.ttype in ('char', 'selection'):
val = value.split(',')[1]
else:
raise Exception('Not implemented')
default_vals[property_.field.name] = val
if not res_ids:
if not names_list:
return default_vals[names[0]]
return default_vals
for name in names:
res[name] = dict((x, default_vals[name]) for x in res_ids)
for property_ in (x for x in properties if x.res):
val = None
if property_.value is not None:
if not isinstance(property_.value, basestring):
val = int(property_.value)
else:
if property_.field.ttype in _CAST:
cast = _CAST[property_.field.ttype]
val = cast(property_.value.split(',')[1])
elif property_.field.ttype in ('char', 'selection'):
val = property_.value.split(',')[1]
else:
raise Exception('Not implemented')
res[property_.field.name][int(property_.res)] = val
if not names_list:
return res[names[0]]
return res
@staticmethod
def _set_values(model, res_id, val, field_id):
return {
'value': val,
'res': model + ',' + str(res_id),
'field': field_id,
}
@classmethod
def set(cls, name, model, ids, val):
"""
Set named property value for ids of model
Return the id of the record created
"""
pool = Pool()
ModelField = pool.get('ir.model.field')
ModelAccess = pool.get('ir.model.access')
ModelAccess.check(model, 'write')
model_field, = ModelField.search([
('name', '=', name),
('model.model', '=', model),
], order=[], limit=1)
Model = pool.get(model)
field = Model._fields[name]
properties = cls.search([
('field', '=', model_field.id),
('res', 'in', [model + ',' + str(res_id) for res_id in ids]),
], order=[])
cls.delete(properties)
defaults = cls.search([
('field', '=', model_field.id),
('res', '=', None),
], order=[], limit=1)
default_val = None
if defaults:
value = cls(defaults[0].id).value
default_val = None
if value is not None:
if not isinstance(value, basestring):
default_val = int(value)
else:
if field._type in _CAST:
cast = _CAST[field._type]
default_val = cast(value.split(',')[1])
elif field._type in ('char', 'selection'):
default_val = value.split(',')[1]
else:
raise Exception('Not implemented')
if (val != default_val):
to_create = []
for res_id in ids:
vals = cls._set_values(model, res_id, val, model_field.id)
to_create.append(vals)
cls.create(to_create)
-56
View File
@@ -1,56 +0,0 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<tryton>
<data>
<record model="ir.ui.view" id="property_view_form">
<field name="model">ir.property</field>
<field name="type">form</field>
<field name="name">property_form</field>
</record>
<record model="ir.ui.view" id="property_view_tree">
<field name="model">ir.property</field>
<field name="type">tree</field>
<field name="name">property_list</field>
</record>
<record model="ir.action.act_window" id="act_property_form_default">
<field name="name">Default Properties</field>
<field name="type">ir.action.act_window</field>
<field name="res_model">ir.property</field>
<field name="domain" eval="[('res', '=', None)]" pyson="1"/>
</record>
<record model="ir.action.act_window.view"
id="act_property_form_view1_default">
<field name="sequence" eval="1"/>
<field name="view" ref="property_view_tree"/>
<field name="act_window" ref="act_property_form_default"/>
</record>
<record model="ir.action.act_window.view"
id="act_property_form_view2_default">
<field name="sequence" eval="2"/>
<field name="view" ref="property_view_form"/>
<field name="act_window" ref="act_property_form_default"/>
</record>
<menuitem parent="ir.menu_models"
action="act_property_form_default" id="menu_property_form_default"/>
<record model="ir.action.act_window" id="act_property_form">
<field name="name">Properties</field>
<field name="type">ir.action.act_window</field>
<field name="res_model">ir.property</field>
</record>
<record model="ir.action.act_window.view"
id="act_property_form_view1">
<field name="sequence" eval="1"/>
<field name="view" ref="property_view_tree"/>
<field name="act_window" ref="act_property_form"/>
</record>
<record model="ir.action.act_window.view"
id="act_property_form_view2">
<field name="sequence" eval="2"/>
<field name="view" ref="property_view_form"/>
<field name="act_window" ref="act_property_form"/>
</record>
<menuitem parent="ir.menu_models"
action="act_property_form" id="menu_property_form"/>
</data>
</tryton>
-1
View File
@@ -15,6 +15,5 @@ xml:
translation.xml
export.xml
rule.xml
property.xml
module.xml
trigger.xml
-11
View File
@@ -1,11 +0,0 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<form>
<label name="field"/>
<field name="field" colspan="3"/>
<label name="value"/>
<field name="value" colspan="3"/>
<label name="res"/>
<field name="res" colspan="3"/>
</form>
-8
View File
@@ -1,8 +0,0 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<tree>
<field name="field"/>
<field name="value"/>
<field name="res"/>
</tree>
+2
View File
@@ -9,10 +9,12 @@ from .workflow import Workflow
from .dictschema import DictSchemaMixin
from .match import MatchMixin
from .union import UnionMixin
from .multivalue import MultiValueMixin, ValueMixin
from .descriptors import dualmethod
from .order import sequence_ordered
__all__ = ['Model', 'ModelView', 'ModelStorage', 'ModelSingleton', 'ModelSQL',
'Check', 'Unique',
'Workflow', 'DictSchemaMixin', 'MatchMixin', 'UnionMixin', 'dualmethod',
'MultiValueMixin', 'ValueMixin',
'EvalEnvironment', 'sequence_ordered']
-1
View File
@@ -16,6 +16,5 @@ from .many2one import *
from .one2many import *
from .many2many import *
from .function import *
from .property import *
from .one2one import *
from .dict import *
+16
View File
@@ -3,6 +3,7 @@
import inspect
import copy
from trytond.model.fields.field import Field
from trytond.tools import is_instance_method
from trytond.transaction import Transaction
@@ -116,3 +117,18 @@ class Function(Field):
def __set__(self, inst, value):
self._field.__set__(inst, value)
class MultiValue(Function):
def __init__(self, field, loading='lazy'):
super(MultiValue, self).__init__(
field, '_multivalue_getter', setter='_multivalue_setter',
loading=loading)
def __copy__(self):
return MultiValue(copy.copy(self._field), loading=self.loading)
def __deepcopy__(self, memo):
return MultiValue(
copy.deepcopy(self._field, memo), loading=self.loading)
-183
View File
@@ -1,183 +0,0 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
import copy
from sql import Cast, Literal, Null
from sql.functions import Substring, Position
from sql.conditionals import Case
from .function import Function
from .field import Field, SQL_OPERATORS
from .numeric import Numeric
from .integer import Integer
from .float import Float
from ...transaction import Transaction
from ...pool import Pool
class Property(Function):
'''
Define a property field that is stored in ir.property (any).
'''
def __init__(self, field):
'''
:param field: The field of the function.
'''
super(Property, self).__init__(field, True, True, True)
__init__.__doc__ += Field.__init__.__doc__
def __copy__(self):
return Property(copy.copy(self._field))
def __deepcopy__(self, memo):
return Property(copy.deepcopy(self._field))
def get(self, ids, model, name, values=None):
'''
Retreive the property.
:param ids: A list of ids.
:param model: The model.
:param name: The name of the field or a list of name field.
:param values:
:return: a dictionary with ids as key and values as value
'''
with Transaction().set_context(_check_access=False):
pool = Pool()
Property = pool.get('ir.property')
return Property.get(name, model.__name__, ids)
def set(self, Model, name, ids, value, *args):
'''
Set the property.
'''
with Transaction().set_context(_check_access=False):
pool = Pool()
Property = pool.get('ir.property')
args = iter((ids, value) + args)
for ids, value in zip(args, args):
if value is not None:
prop_value = '%s,%s' % (getattr(self, 'model_name', ''),
str(value))
else:
prop_value = None
# TODO change set API to use sequence of records, value
Property.set(name, Model.__name__, ids, prop_value)
def convert_domain(self, domain, tables, Model):
pool = Pool()
Rule = pool.get('ir.rule')
Property = pool.get('ir.property')
IrModel = pool.get('ir.model')
Field = pool.get('ir.model.field')
cursor = Transaction().connection.cursor()
name, operator, value = domain
sql_type = self._field.sql_type().base
property_cond = Rule.query_get('ir.property')
property_ = Property.__table__()
model_field = Field.__table__()
model = IrModel.__table__()
# Fetch res ids that comply with the domain
join = property_.join(model_field,
condition=model_field.id == property_.field)
join = join.join(model,
condition=model.id == model_field.model)
cond = ((model.model == Model.__name__)
& (model_field.name == name))
if property_cond:
cond &= property_.id.in_(property_cond)
cursor.execute(*join.select(
Cast(Substring(property_.res,
Position(',', property_.res) + Literal(1)),
Model.id.sql_type().base),
property_.id,
# Use a Case because the condition created by get_condition
# could result in an invalid Cast
where=Case(
(cond, self.get_condition(sql_type, domain, property_)),
else_=(Literal(1) == Literal(0)))))
props = cursor.fetchall()
default = None
for prop in props:
if not prop[0]:
default = prop[1]
break
if (not default
or ((value is False or value is None)
and operator in ['=', '!='])
or (operator in ['not like', 'not ilike', 'not in', '!='])):
dom_operator = 'in' # default operator
if (((value is False or value is None) and operator == '=')
or ((value is not False and value is not None)
and operator in [
'not like', 'not ilike', 'not in', '!='])):
dom_operator = 'not in'
return [('id', dom_operator, [x[0] for x in props])]
# Fetch the res ids that doesn't use the default value
cursor.execute(*property_.select(
Cast(Substring(property_.res,
Position(',', property_.res) + Literal(1)),
Model.id.sql_type().base),
where=property_cond & (property_.res != Null)))
fetchall = cursor.fetchall()
if not fetchall:
return [('id', 'in', [x[0] for x in props])]
else:
other_ids = [x[0] for x in fetchall]
res_ids = Model.search(['OR',
('id', 'in', [x[0] for x in props]),
('id', 'not in', other_ids)
])
return [('id', 'in', res_ids)]
@staticmethod
def get_condition(sql_type, clause, table):
operator = clause[1]
value = clause[2]
sql_types = [
Numeric('numeric').sql_type().base,
Integer('integer').sql_type().base,
Float('float').sql_type().base,
]
if sql_type in sql_types and value:
if isinstance(value, (list, tuple)):
value = [Cast(v, sql_type) for v in value]
else:
value = Cast(value, sql_type)
if value is None:
value = False
column = Cast(Substring(table.value,
Position(',', table.value) + Literal(1)),
sql_type)
Operator = SQL_OPERATORS[operator]
# All negative clauses will be negated later
if operator in ('in', 'not in'):
return column.in_(value)
elif ((value is False or value is None)
and operator in ('=', '!=')):
return column != Null
elif operator == 'not like':
return column.like(value)
elif operator == 'not ilike':
return column.ilike(value)
elif operator == '!=':
return column == value
return Operator(column, value)
+3 -3
View File
@@ -4,16 +4,16 @@
class MatchMixin(object):
def match(self, pattern):
def match(self, pattern, match_none=False):
'''Match on pattern
pattern is a dictionary with model field as key
and matching value as value'''
for field, pattern_value in pattern.iteritems():
value = getattr(self, field)
if value is None:
if not match_none and value is None:
continue
if self._fields[field]._type == 'many2one':
value = value.id
value = value.id if value else value
if value != pattern_value:
return False
return True
+1 -7
View File
@@ -123,7 +123,6 @@ class Model(WarningErrorMixin, URLMixin, PoolBase):
If with_rec_name is True, rec_name will be added.
'''
pool = Pool()
Property = pool.get('ir.property')
value = {}
default_rec_name = Transaction().context.get('default_rec_name')
@@ -140,8 +139,6 @@ class Model(WarningErrorMixin, URLMixin, PoolBase):
if (field._type == 'boolean'
and field_name not in value):
value[field_name] = False
if isinstance(field, fields.Property):
value[field_name] = Property.get(field_name, cls.__name__)
if (with_rec_name
and field._type in ('many2one',)
and value.get(field_name)):
@@ -243,10 +240,7 @@ class Model(WarningErrorMixin, URLMixin, PoolBase):
if ((isinstance(cls._fields[field], fields.Function)
and not (cls._fields[field].searcher
or getattr(cls, 'domain_%s' % field, None)))
or (cls._fields[field]._type in ('binary', 'sha'))
or (isinstance(cls._fields[field], fields.Property)
and isinstance(cls._fields[field]._field,
fields.Many2One))):
or (cls._fields[field]._type in ('binary', 'sha'))):
res[field]['searchable'] = False
else:
res[field]['searchable'] = True
+3 -3
View File
@@ -286,8 +286,8 @@ class ModelStorage(Model):
if field_name in default:
data[field_name] = default[field_name]
elif (isinstance(cls._fields[field_name], fields.Function)
and not isinstance(cls._fields[field_name],
fields.Property)):
and not isinstance(
cls._fields[field_name], fields.MultiValue)):
del data[field_name]
elif ftype in ('many2one', 'one2one'):
try:
@@ -315,7 +315,7 @@ class ModelStorage(Model):
mptt.add(field.right)
fields_names = [n for n, f in cls._fields.iteritems()
if (not isinstance(f, fields.Function)
or isinstance(f, fields.Property))
or isinstance(f, fields.MultiValue))
and n not in mptt]
ids = map(int, records)
datas = cls.read(ids, fields_names=fields_names)
+87
View File
@@ -0,0 +1,87 @@
# This file is part of Tryton. The COPYRIGHT file at the toplevel of this
# repository contains the full copyright notices and license terms.
from ..pool import Pool
from .model import Model
from .match import MatchMixin
class MultiValueMixin(object):
@classmethod
def multivalue_model(cls, field):
pool = Pool()
Value = pool.get('%s.%s' % (cls.__name__, field))
assert issubclass(Value, ValueMixin), (
"%s is not a subclass of ValueMixin" % Value)
return Value
def multivalue_records(self, field):
Value = self.multivalue_model(field)
for fname, field in self._fields.iteritems():
if (field._type == 'one2many'
and field.model_name == Value.__name__):
return getattr(self, fname)
return Value.search([])
def multivalue_record(self, field, **pattern):
Value = self.multivalue_model(field)
for fname, field in Value._fields.iteritems():
if (field._type == 'many2one'
and field.model_name == self.__name__):
pattern = pattern.copy()
pattern[fname] = self
break
return Value(**pattern)
def __values(self, field, pattern, match_none=True):
return [v for v in self.multivalue_records(field)
if v.match(pattern, match_none=match_none)]
def get_multivalue(self, name, **pattern):
values = self.__values(name, pattern, match_none=False)
if not values:
Value = self.multivalue_model(name)
value = Value(**pattern)
func = getattr(self, 'default_%s' % name, lambda **kw: None)
setattr(value, name, func(**pattern))
else:
value = values[0]
return getattr(value, name)
def _multivalue_getter(self, name):
Value = self.multivalue_model(name)
value = self.get_multivalue(name)
if isinstance(value, Model):
if Value._fields[name]._type == 'reference':
return str(value)
return value.id
elif isinstance(value, (list, tuple)):
return [r.id for r in value]
else:
return value
def set_multivalue(self, name, value, _save=True, **pattern):
Value = self.multivalue_model(name)
values = self.__values(name, pattern, match_none=True)
if not values:
values = [self.multivalue_record(name, **pattern)]
for record in values:
setattr(record, name, value)
if _save:
Value.save(values)
else:
return values
@classmethod
def _multivalue_setter(cls, records, name, val):
Value = cls.multivalue_model(name)
to_save = []
for record in records:
to_save.extend(record.set_multivalue(name, val, _save=False))
Value.save(to_save)
class ValueMixin(MatchMixin):
def match(self, pattern, match_none=True):
return super(ValueMixin, self).match(pattern, match_none=match_none)
-23
View File
@@ -371,21 +371,6 @@ this repository contains the full copyright notices and license terms. -->
<field name="perm_create" eval="True"/>
<field name="perm_delete" eval="True"/>
</record>
<record model="ir.model.access" id="access_ir_property">
<field name="model" search="[('model', '=', 'ir.property')]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_delete" eval="False"/>
</record>
<record model="ir.model.access" id="access_ir_property_admin">
<field name="model" search="[('model', '=', 'ir.property')]"/>
<field name="group" ref="group_admin"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_delete" eval="True"/>
</record>
<record model="ir.model.access" id="access_ir_module">
<field name="model" search="[('model', '=', 'ir.module')]"/>
<field name="perm_read" eval="True"/>
@@ -694,14 +679,6 @@ this repository contains the full copyright notices and license terms. -->
<field name="menu" ref="ir.menu_rule_group_form"/>
<field name="group" ref="group_admin"/>
</record>
<record model="ir.ui.menu-res.group" id="menu_property_form_default_group_admin">
<field name="menu" ref="ir.menu_property_form_default"/>
<field name="group" ref="group_admin"/>
</record>
<record model="ir.ui.menu-res.group" id="menu_property_form_group_admin">
<field name="menu" ref="ir.menu_property_form"/>
<field name="group" ref="group_admin"/>
</record>
<record model="ir.ui.menu-res.group" id="menu_modules_group_admin">
<field name="menu" ref="ir.menu_modules"/>
<field name="group" ref="group_admin"/>
+3 -1
View File
@@ -15,6 +15,7 @@ from .workflow import *
from .copy_ import *
from history import *
from .field_context import *
from . import multivalue
def register():
@@ -101,7 +102,6 @@ def register():
Reference,
ReferenceTarget,
ReferenceRequired,
Property,
Selection,
SelectionRequired,
DictSchema,
@@ -194,6 +194,8 @@ def register():
TestWizard,
module='tests', type_='wizard')
multivalue.register('tests')
def suite():
from .test_tryton import all_suite
+32
View File
@@ -0,0 +1,32 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from trytond.model import ModelSQL, MultiValueMixin, ValueMixin, fields
from trytond.pool import Pool
class ModelMultiValue(ModelSQL, MultiValueMixin):
"Model MultiValue"
__name__ = 'test.model_multivalue'
value = fields.MultiValue(fields.Char("Value"))
values = fields.One2Many(
'test.model_multivalue.value', 'record', "Values")
@classmethod
def default_value(cls, **pattern):
return "default"
class ModelValue(ModelSQL, ValueMixin):
"Model Value"
__name__ = 'test.model_multivalue.value'
record = fields.Many2One(
'test.model_multivalue', "Record")
condition = fields.Char("Condition")
value = fields.Char("Value")
def register(module):
Pool.register(
ModelMultiValue,
ModelValue,
module=module, type_='model')
-15
View File
@@ -38,7 +38,6 @@ __all__ = [
'Many2ManyFilterDomain', 'Many2ManyFilterDomainTarget',
'Many2ManyFilterDomainRelation',
'Reference', 'ReferenceTarget', 'ReferenceRequired',
'Property',
'Selection', 'SelectionRequired',
'DictSchema', 'Dict', 'DictDefault', 'DictRequired',
'Binary', 'BinaryDefault', 'BinaryRequired', 'BinaryFileStorage',
@@ -720,20 +719,6 @@ class ReferenceRequired(ModelSQL):
], required=True)
class Property(ModelSQL):
'Property'
__name__ = 'test.property'
char = fields.Property(fields.Char('Test Char'))
many2one = fields.Property(fields.Many2One('test.char',
'Test Many2One'))
numeric = fields.Property(fields.Numeric('Test Numeric'))
selection = fields.Property(fields.Selection([
(None, ''),
('option_a', 'Option A'),
('option_b', 'Option B')
], 'Test Selection'))
class Selection(ModelSQL):
'Selection'
__name__ = 'test.selection'
-270
View File
@@ -2999,276 +2999,6 @@ class FieldsTestCase(unittest.TestCase):
}])
self.assert_(reference4)
@with_transaction()
def test_property(self):
'Test Property with supported field types'
pool = Pool()
Property = pool.get('test.property')
IrProperty = pool.get('ir.property')
ModelField = pool.get('ir.model.field')
Char = pool.get('test.char')
transaction = Transaction()
# Test Char
prop_a, = Property.create([{'char': 'Test'}])
self.assert_(prop_a)
self.assertEqual(prop_a.char, 'Test')
prop_b, = Property.create([{}])
self.assert_(prop_b)
self.assertEqual(prop_b.char, None)
prop_c, = Property.create([{'char': 'FooBar'}])
self.assert_(prop_c)
self.assertEqual(prop_c.char, 'FooBar')
props = Property.search([('char', '=', 'Test')])
self.assertEqual(props, [prop_a])
props = Property.search([('char', '=', None)])
self.assertEqual(props, [prop_b])
props = Property.search([('char', '!=', None)])
self.assertEqual(props, [prop_a, prop_c])
props = Property.search([('char', 'like', 'Tes%')])
self.assertEqual(props, [prop_a])
props = Property.search([('char', 'like', '%Bar')])
self.assertEqual(props, [prop_c])
props = Property.search([('char', 'not like', 'Tes%')])
self.assertEqual(props, [prop_b, prop_c])
props = Property.search([('char', 'ilike', 'tes%')])
self.assert_(props, [prop_a])
props = Property.search([('char', 'ilike', '%bar')])
self.assertEqual(props, [prop_c])
props = Property.search([('char', 'not ilike', 'tes%')])
self.assertEqual(props, [prop_b, prop_c])
props = Property.search([('char', 'in', ['Test'])])
self.assertEqual(props, [prop_a])
props = Property.search([
('char', 'in', ['Test', 'FooBar'])])
self.assertEqual(props, [prop_a, prop_c])
props = Property.search([
('char', 'not in', ['Test', 'FooBar'])])
self.assertEqual(props, [prop_b])
# Test default value
property_field, = ModelField.search([
('model.model', '=', 'test.property'),
('name', '=', 'char'),
], limit=1)
IrProperty.create([{
'field': property_field.id,
'value': ',DEFAULT_VALUE',
}])
prop_d, = Property.create([{}])
self.assert_(prop_d)
self.assertEqual(prop_d.char, 'DEFAULT_VALUE')
props = Property.search([('char', '!=', None)])
self.assertEqual(props, [prop_a, prop_c, prop_d])
Property.write([prop_a], {'char': None})
self.assertEqual(prop_a.char, None)
Property.write([prop_b], {'char': 'Test'})
self.assertEqual(prop_b.char, 'Test')
transaction.rollback()
# Test Many2One
char_a, = Char.create([{'char': 'Test'}])
self.assert_(char_a)
char_b, = Char.create([{'char': 'FooBar'}])
self.assert_(char_b)
prop_a, = Property.create([{'many2one': char_a.id}])
self.assert_(prop_a)
self.assertEqual(prop_a.many2one, char_a)
prop_b, = Property.create([{'many2one': char_b.id}])
self.assert_(prop_b)
self.assertEqual(prop_b.many2one, char_b)
prop_c, = Property.create([{}])
self.assert_(prop_c)
self.assertEqual(prop_c.many2one, None)
props = Property.search([('many2one', '=', char_a.id)])
self.assertEqual(props, [prop_a])
props = Property.search([('many2one', '!=', None)])
self.assertEqual(props, [prop_a, prop_b])
props = Property.search([('many2one', '=', None)])
self.assertEqual(props, [prop_c])
self.assertEqual(prop_a.many2one, char_a)
props = Property.search([
('many2one', 'in', [char_a.id, char_b.id])])
self.assertEqual(props, [prop_a, prop_b])
props = Property.search([
('many2one', 'not in', [char_a.id, char_b.id])])
self.assertEqual(props, [prop_c])
Property.write([prop_b], {'many2one': char_a.id})
self.assertEqual(prop_b.many2one, char_a)
transaction.rollback()
# Test Numeric
prop_a, = Property.create([{'numeric': Decimal('1.1')}])
self.assert_(prop_a)
self.assertEqual(prop_a.numeric, Decimal('1.1'))
prop_b, = Property.create([{'numeric': Decimal('2.6')}])
self.assert_(prop_b)
self.assertEqual(prop_b.numeric, Decimal('2.6'))
prop_c, = Property.create([{}])
self.assert_(prop_c)
self.assertEqual(prop_c.numeric, None)
props = Property.search([('numeric', '!=', None)])
self.assertEqual(props, [prop_a, prop_b])
props = Property.search([('numeric', '=', None)])
self.assertEqual(props, [prop_c])
props = Property.search([
('numeric', '=', Decimal('1.1')),
])
self.assertEqual(props, [prop_a])
props = Property.search([
('numeric', '!=', Decimal('1.1'))])
self.assertEqual(props, [prop_b, prop_c])
props = Property.search([
('numeric', '<', Decimal('2.6')),
])
self.assertEqual(props, [prop_a])
props = Property.search([
('numeric', '<=', Decimal('2.6'))])
self.assertEqual(props, [prop_a, prop_b])
props = Property.search([
('numeric', '>', Decimal('1.1')),
])
self.assertEqual(props, [prop_b])
props = Property.search([
('numeric', '>=', Decimal('1.1'))])
self.assertEqual(props, [prop_a, prop_b])
props = Property.search([
('numeric', 'in', [Decimal('1.1')])])
self.assertEqual(props, [prop_a])
props = Property.search([
('numeric', 'in', [Decimal('1.1'), Decimal('2.6')])])
self.assertEqual(props, [prop_a, prop_b])
props = Property.search([
('numeric', 'not in', [Decimal('1.1')])])
self.assertEqual(props, [prop_b, prop_c])
props = Property.search([
('numeric', 'not in', [Decimal('1.1'), Decimal('2.6')])])
self.assertEqual(props, [prop_c])
# Test default value
property_field, = ModelField.search([
('model.model', '=', 'test.property'),
('name', '=', 'numeric'),
], limit=1)
IrProperty.create([{
'field': property_field.id,
'value': ',3.7',
}])
prop_d, = Property.create([{}])
self.assert_(prop_d)
self.assertEqual(prop_d.numeric, Decimal('3.7'))
Property.write([prop_a], {'numeric': None})
self.assertEqual(prop_a.numeric, None)
Property.write([prop_b], {'numeric': Decimal('3.11')})
self.assertEqual(prop_b.numeric, Decimal('3.11'))
transaction.rollback()
# Test Selection
prop_a, = Property.create([{'selection': 'option_a'}])
self.assert_(prop_a)
self.assertEqual(prop_a.selection, 'option_a')
prop_b, = Property.create([{'selection': 'option_b'}])
self.assert_(prop_b)
self.assertEqual(prop_b.selection, 'option_b')
prop_c, = Property.create([{}])
self.assert_(prop_c)
self.assertEqual(prop_c.selection, None)
props = Property.search([('selection', '=', 'option_a')])
self.assertEqual(props, [prop_a])
props = Property.search([('selection', '!=', None)])
self.assertEqual(props, [prop_a, prop_b])
props = Property.search([('selection', '=', None)])
self.assertEqual(props, [prop_c])
props = Property.search([('selection', '!=', 'option_a')])
self.assertEqual(props, [prop_b, prop_c])
props = Property.search([
('selection', 'in', ['option_a'])])
self.assertEqual(props, [prop_a])
props = Property.search([
('selection', 'in', ['option_a', 'option_b'])])
self.assertEqual(props, [prop_a, prop_b])
props = Property.search([
('selection', 'not in', ['option_a'])])
self.assertEqual(props, [prop_b, prop_c])
# Test default value
property_field, = ModelField.search([
('model.model', '=', 'test.property'),
('name', '=', 'selection'),
], limit=1)
IrProperty.create([{
'field': property_field.id,
'value': ',option_a',
}])
prop_d, = Property.create([{}])
self.assert_(prop_d)
self.assertEqual(prop_d.selection, 'option_a')
Property.write([prop_a], {'selection': None})
self.assertEqual(prop_a.selection, None)
Property.write([prop_c], {'selection': 'option_b'})
self.assertEqual(prop_c.selection, 'option_b')
@with_transaction()
def test_selection(self):
'Test Selection'
+115
View File
@@ -0,0 +1,115 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
import unittest
from trytond.tests.test_tryton import activate_module, with_transaction
from trytond.pool import Pool
class MultiValueTestCase(unittest.TestCase):
"Test MultiValue"
@classmethod
def setUpClass(cls):
activate_module('tests')
@with_transaction()
def test_get_multivalue(self):
"Test get_multivalue"
pool = Pool()
ModelMultiValue = pool.get('test.model_multivalue')
ModelValue = pool.get('test.model_multivalue.value')
record = ModelMultiValue()
record.save()
value, = ModelValue.search([])
value.condition = "foo"
value.value = "bar"
value.save()
self.assertEqual(
record.get_multivalue('value', condition="foo"),
"bar")
@with_transaction()
def test_get_multivalue_default(self):
"Test get_multivalue default value"
pool = Pool()
ModelMultiValue = pool.get('test.model_multivalue')
record = ModelMultiValue()
record.save()
self.assertEqual(record.get_multivalue('value'), "default")
@with_transaction()
def test_get_multivalue_match_none(self):
"Test get_multivalue does not match None"
pool = Pool()
ModelMultiValue = pool.get('test.model_multivalue')
ModelValue = pool.get('test.model_multivalue.value')
record = ModelMultiValue()
record.save()
value = ModelValue(record=record, condition="foo", value="bar")
value.save()
self.assertEqual(
record.get_multivalue('value', condition="test"),
"default")
@with_transaction()
def test_set_multivalue(self):
"Test set_multivalue"
pool = Pool()
ModelMultiValue = pool.get('test.model_multivalue')
ModelValue = pool.get('test.model_multivalue.value')
record = ModelMultiValue()
record.save()
record.set_multivalue('value', "set", condition="test")
value, = ModelValue.search([('condition', '=', "test")])
self.assertEqual(value.record, record)
self.assertEqual(value.value, "set")
@with_transaction()
def test_set_multivalue_match_none(self):
"Test set_multivalue matches None"
pool = Pool()
ModelMultiValue = pool.get('test.model_multivalue')
ModelValue = pool.get('test.model_multivalue.value')
record = ModelMultiValue()
record.save()
record.set_multivalue('value', "set", condition="test")
self.assertEqual(len(ModelValue.search([])), 2)
@with_transaction()
def test_mutlivalue_setter(self):
"Test multivalue setter"
pool = Pool()
ModelMultiValue = pool.get('test.model_multivalue')
record, = ModelMultiValue.create([{
'value': "setter",
}])
self.assertEqual(record.get_multivalue('value'), "setter")
@with_transaction()
def test_mutlivalue_getter(self):
"Test multivalue getter"
pool = Pool()
ModelMultiValue = pool.get('test.model_multivalue')
record = ModelMultiValue(value="getter")
record.save()
read, = ModelMultiValue.read([record.id], ['value'])
self.assertEqual(read['value'], "getter")
def suite():
return unittest.TestLoader().loadTestsFromTestCase(MultiValueTestCase)
+111
View File
@@ -0,0 +1,111 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from decimal import Decimal
from sql import Table, Column, Literal, Union, Null
from sql.aggregate import Max
from trytond import backend
from trytond.pool import Pool
from trytond.transaction import Transaction
def migrate_property(
model_name, field_names, ValueModel, value_names,
parent=None, fields=None):
"Migrate property from model_name.field_name to ValueModel.value_name"
pool = Pool()
Field = pool.get('ir.model.field')
Model = pool.get('ir.model')
TableHandler = backend.get('TableHandler')
if not TableHandler.table_exist('ir_property'):
return
cursor = Transaction().connection.cursor()
field = Field.__table__()
model = Model.__table__()
table = ValueModel.__table__()
if fields is None:
fields = []
if isinstance(field_names, basestring):
field_names = [field_names]
if isinstance(value_names, basestring):
value_names = [value_names]
def split_value(value):
return value.split(',')[1]
cast_funcs = {
'numeric': lambda v: Decimal(split_value(v)) if v else None,
'integer': lambda v: int(split_value(v)) if v else None,
'float': lambda v: float(split_value(v)) if v else None,
'char': lambda v: split_value(v) if v else None,
'selection': lambda v: split_value(v) if v else None,
'many2one': lambda v: int(split_value(v)) if v else None,
'reference': lambda v: v,
}
casts = []
queries = []
for field_name, value_name in zip(field_names, value_names):
value_field = getattr(ValueModel, value_name)
casts.append(cast_funcs[value_field._type])
property_ = Table('ir_property')
columns = [
Literal(None).as_(f) if f != value_name
else property_.value.as_(value_name)
for f in value_names]
if parent:
columns.append(property_.res.as_(parent))
where = property_.res.like(model_name + ',%')
else:
where = property_.res == Null
columns.extend([Column(property_, f).as_(f) for f in fields])
query = property_.join(field,
condition=property_.field == field.id
).join(model,
condition=field.model == model.id
).select(*columns,
where=where
& (field.name == field_name)
& (model.model == model_name))
queries.append(query)
union = Union(*queries)
columns = [Max(Column(union, f)).as_(f) for f in value_names]
if parent:
columns.append(Column(union, parent).as_(parent))
pcolumns = [Column(union, parent)]
else:
pcolumns = []
vcolumns = [Column(union, f).as_(f) for f in fields]
cursor.execute(
*union.select(*(columns + vcolumns), group_by=pcolumns + vcolumns))
columns = [Column(table, f) for f in value_names]
if parent:
pcolumns = [Column(table, parent)]
else:
pcolumns = []
vcolumns = [Column(table, f) for f in fields]
values = []
l = len(value_names)
for row in cursor.fetchall():
value = [c(v) for v, c in zip(row, casts)]
if parent:
value.append(int(row[l].split(',')[1]) if row[l] else None)
i = 1
else:
i = 0
value.extend(row[l + i:])
values.append(value)
if (values and not (
# No property defined
len(values) == 1
and all(x is None for x in values[0][:len(columns)]))):
# Delete previous migrated values
cursor.execute(*table.delete())
cursor.execute(*table.insert(
columns + pcolumns + vcolumns, values=values))