Files
tryton/trytond/ir/ui/view.py
T
2016-03-03 21:51:57 +01:00

431 lines
14 KiB
Python

# 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 os
import sys
import logging
try:
import simplejson as json
except ImportError:
import json
from lxml import etree
from trytond.model import ModelView, ModelSQL, fields
from trytond import backend
from trytond.pyson import Eval, Bool, PYSONDecoder, If
from trytond.tools import file_open
from trytond.transaction import Transaction
from trytond.wizard import Wizard, StateView, Button
from trytond.pool import Pool
from trytond.cache import Cache
from trytond.rpc import RPC
__all__ = [
'View', 'ShowViewStart', 'ShowView',
'ViewTreeWidth', 'ViewTreeState', 'ViewSearch',
]
logger = logging.getLogger(__name__)
class View(ModelSQL, ModelView):
"View"
__name__ = 'ir.ui.view'
_rec_name = 'model'
model = fields.Char('Model', select=True, states={
'required': Eval('type').in_([None, 'tree', 'form', 'graph']),
})
priority = fields.Integer('Priority', required=True, select=True)
type = fields.Selection([
(None, ''),
('tree', 'Tree'),
('form', 'Form'),
('graph', 'Graph'),
('calendar', 'Calendar'),
('board', 'Board'),
], 'View Type', select=True,
domain=[
If(Bool(Eval('inherit')),
('type', '=', None),
('type', '!=', None)),
],
depends=['inherit'])
data = fields.Text('Data')
name = fields.Char('Name', states={
'invisible': ~(Eval('module') & Eval('name')),
}, depends=['module'], readonly=True)
arch = fields.Function(fields.Text('View Architecture', states={
'readonly': Bool(Eval('name')),
}, depends=['name']), 'get_arch', setter='set_arch')
inherit = fields.Many2One('ir.ui.view', 'Inherited View', select=True,
ondelete='CASCADE')
field_childs = fields.Char('Children Field', states={
'invisible': Eval('type') != 'tree',
}, depends=['type'])
module = fields.Char('Module', states={
'invisible': ~Eval('module'),
}, readonly=True)
domain = fields.Char('Domain', states={
'invisible': ~Eval('inherit'),
}, depends=['inherit'])
_get_rng_cache = Cache('ir_ui_view.get_rng')
@classmethod
def __setup__(cls):
super(View, cls).__setup__()
cls._error_messages.update({
'invalid_xml': 'Invalid XML for view "%s".',
})
cls._order.insert(0, ('priority', 'ASC'))
cls._buttons.update({
'show': {
'readonly': Eval('type') != 'form',
},
})
@classmethod
def __register__(cls, module_name):
TableHandler = backend.get('TableHandler')
table = TableHandler(cls, module_name)
# Migration from 2.4 arch moved into data
if table.column_exist('arch'):
table.column_rename('arch', 'data')
super(View, cls).__register__(module_name)
# New instance to refresh definition
table = TableHandler(cls, module_name)
# Migration from 1.0 arch no more required
table.not_null_action('arch', action='remove')
# Migration from 2.4 model no more required
table.not_null_action('model', action='remove')
@staticmethod
def default_priority():
return 16
@staticmethod
def default_module():
return Transaction().context.get('module') or ''
@classmethod
@ModelView.button_action('ir.act_view_show')
def show(cls, views):
pass
@classmethod
def get_rng(cls, type_):
key = (cls.__name__, type_)
rng = cls._get_rng_cache.get(key)
if rng is None:
if sys.version_info < (3,):
filename = __file__.decode(sys.getfilesystemencoding())
else:
filename = __file__
rng_name = os.path.join(os.path.dirname(filename), type_ + '.rng')
with open(rng_name, 'rb') as fp:
rng = etree.fromstring(fp.read())
cls._get_rng_cache.set(key, rng)
return rng
@property
def rng_type(self):
if self.inherit:
return self.inherit.rng_type
return self.type
@classmethod
def validate(cls, views):
super(View, cls).validate(views)
cls.check_xml(views)
@classmethod
def check_xml(cls, views):
"Check XML"
for view in views:
if not view.arch:
continue
xml = view.arch.strip()
if not xml:
continue
tree = etree.fromstring(xml)
if hasattr(etree, 'RelaxNG'):
validator = etree.RelaxNG(etree=cls.get_rng(view.rng_type))
if not validator.validate(tree):
error_log = '\n'.join(map(str,
validator.error_log.filter_from_errors()))
logger.error('Invalid XML view %s:\n%s\n%s',
view.rec_name, error_log, xml)
cls.raise_user_error(
'invalid_xml', (view.rec_name,), error_log)
root_element = tree.getroottree().getroot()
# validate pyson attributes
validates = {
'states': fields.states_validate,
}
def encode(element):
for attr in ('states', 'domain', 'spell'):
if not element.get(attr):
continue
try:
value = PYSONDecoder().decode(element.get(attr))
validates.get(attr, lambda a: True)(value)
except Exception, e:
error_log = '%s: <%s %s="%s"/>' % (
e, element.get('id') or element.get('name'), attr,
value)
logger.error(
'Invalid XML view %s:\n%s\n%s',
view.rec_name, error_log, xml)
cls.raise_user_error(
'invalid_xml', (view.rec_name,), error_log)
for child in element:
encode(child)
encode(root_element)
def get_arch(self, name):
value = None
if self.name and self.module:
path = os.path.join(self.module, 'view', self.name + '.xml')
try:
with file_open(path, subdir='modules', mode='rb') as fp:
value = fp.read()
except IOError:
pass
if not value:
value = self.data
return value
@classmethod
def set_arch(cls, views, name, value):
cls.write(views, {'data': value})
@classmethod
def delete(cls, views):
super(View, cls).delete(views)
# Restart the cache
ModelView._fields_view_get_cache.clear()
@classmethod
def create(cls, vlist):
views = super(View, cls).create(vlist)
# Restart the cache
ModelView._fields_view_get_cache.clear()
return views
@classmethod
def write(cls, views, values, *args):
super(View, cls).write(views, values, *args)
# Restart the cache
ModelView._fields_view_get_cache.clear()
class ShowViewStart(ModelView):
'Show view'
__name__ = 'ir.ui.view.show.start'
class ShowView(Wizard):
'Show view'
__name__ = 'ir.ui.view.show'
class ShowStateView(StateView):
def __init__(self, model_name, buttons):
StateView.__init__(self, model_name, None, buttons)
def get_view(self, wizard, state_name):
pool = Pool()
View = pool.get('ir.ui.view')
view = View(Transaction().context.get('active_id'))
Model = pool.get(view.model)
return Model.fields_view_get(view_id=view.id)
def get_defaults(self, wizard, state_name, fields):
return {}
start = ShowStateView('ir.ui.view.show.start', [
Button('Close', 'end', 'tryton-close', default=True),
])
class ViewTreeWidth(ModelSQL, ModelView):
"View Tree Width"
__name__ = 'ir.ui.view_tree_width'
_rec_name = 'model'
model = fields.Char('Model', required=True, select=True)
field = fields.Char('Field', required=True, select=True)
user = fields.Many2One('res.user', 'User', required=True,
ondelete='CASCADE', select=True)
width = fields.Integer('Width', required=True)
@classmethod
def __setup__(cls):
super(ViewTreeWidth, cls).__setup__()
cls.__rpc__.update({
'set_width': RPC(readonly=False),
})
@classmethod
def delete(cls, records):
ModelView._fields_view_get_cache.clear()
super(ViewTreeWidth, cls).delete(records)
@classmethod
def create(cls, vlist):
res = super(ViewTreeWidth, cls).create(vlist)
ModelView._fields_view_get_cache.clear()
return res
@classmethod
def write(cls, records, values, *args):
super(ViewTreeWidth, cls).write(records, values, *args)
ModelView._fields_view_get_cache.clear()
@classmethod
def set_width(cls, model, fields):
'''
Set width for the current user on the model.
fields is a dictionary with key: field name and value: width.
'''
records = cls.search([
('user', '=', Transaction().user),
('model', '=', model),
('field', 'in', fields.keys()),
])
cls.delete(records)
to_create = []
for field in fields.keys():
to_create.append({
'model': model,
'field': field,
'user': Transaction().user,
'width': fields[field],
})
if to_create:
cls.create(to_create)
class ViewTreeState(ModelSQL, ModelView):
'View Tree State'
__name__ = 'ir.ui.view_tree_state'
_rec_name = 'model'
model = fields.Char('Model', required=True)
domain = fields.Char('Domain', required=True)
user = fields.Many2One('res.user', 'User', required=True,
ondelete='CASCADE')
child_name = fields.Char('Child Name')
nodes = fields.Text('Expanded Nodes')
selected_nodes = fields.Text('Selected Nodes')
@classmethod
def __setup__(cls):
super(ViewTreeState, cls).__setup__()
cls.__rpc__.update({
'set': RPC(readonly=False, check_access=False),
'get': RPC(check_access=False),
})
@classmethod
def __register__(cls, module_name):
TableHandler = backend.get('TableHandler')
table = TableHandler(cls, module_name)
# Migration from 2.8: table name changed
table.table_rename('ir_ui_view_tree_expanded_state', cls._table)
super(ViewTreeState, cls).__register__(module_name)
table = TableHandler(cls, module_name)
table.index_action(['model', 'domain', 'user', 'child_name'], 'add')
@staticmethod
def default_nodes():
return '[]'
@staticmethod
def default_selected_nodes():
return '[]'
@classmethod
def set(cls, model, domain, child_name, nodes, selected_nodes):
# Normalize the json domain
domain = json.dumps(json.loads(domain))
current_user = Transaction().user
records = cls.search([
('user', '=', current_user),
('model', '=', model),
('domain', '=', domain),
('child_name', '=', child_name),
])
cls.delete(records)
cls.create([{
'user': current_user,
'model': model,
'domain': domain,
'child_name': child_name,
'nodes': nodes,
'selected_nodes': selected_nodes,
}])
@classmethod
def get(cls, model, domain, child_name):
# Normalize the json domain
domain = json.dumps(json.loads(domain))
current_user = Transaction().user
try:
expanded_info, = cls.search([
('user', '=', current_user),
('model', '=', model),
('domain', '=', domain),
('child_name', '=', child_name),
],
limit=1)
except ValueError:
return (cls.default_nodes(), cls.default_selected_nodes())
state = cls(expanded_info)
return (state.nodes or cls.default_nodes(),
state.selected_nodes or cls.default_selected_nodes())
class ViewSearch(ModelSQL, ModelView):
"View Search"
__name__ = 'ir.ui.view_search'
name = fields.Char('Name', required=True)
model = fields.Char('Model', required=True)
domain = fields.Char('Domain', help="The PYSON domain")
user = fields.Many2One('res.user', 'User', required=True,
ondelete='CASCADE')
@classmethod
def __setup__(cls):
super(ViewSearch, cls).__setup__()
cls.__rpc__.update({
'get_search': RPC(),
})
@staticmethod
def default_user():
return Transaction().user
@classmethod
def get_search(cls, user_id=None):
if user_id is None:
user_id = Transaction().user
decoder = PYSONDecoder()
searches = cls.search([
('user', '=', user_id),
], order=[('model', 'ASC'), ('name', 'ASC')])
result = {}
for search in searches:
result.setdefault(search.model, []).append(
(search.id, search.name, decoder.decode(search.domain)))
return result