Files
tryton/trytond/ir/ui/view.py
T
2018-12-29 14:19:08 +01:00

408 lines
13 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 logging
import json
from lxml import etree
from trytond.i18n import gettext
from trytond.model import ModelView, ModelSQL, fields
from trytond.model.exceptions import ValidationError
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 XMLError(ValidationError):
pass
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._order.insert(0, ('priority', 'ASC'))
cls._buttons.update({
'show': {
'readonly': Eval('type') != 'form',
'depends': ['type'],
},
})
@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:
rng_name = os.path.join(os.path.dirname(__file__), 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)
raise XMLError(
gettext('ir.msg_view_invalid_xml', name=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 as e:
error_log = '%s: <%s %s="%s"/>' % (
e, element.get('id') or element.get('name'), attr,
element.get(attr))
logger.error(
'Invalid XML view %s:\n%s\n%s',
view.rec_name, error_log, xml)
raise XMLError(
gettext(
'ir.msg_view_invalid_xml', name=view.rec_name),
error_log) from e
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='r', encoding='utf-8') 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_id = Transaction().context.get('active_id')
if not view_id:
return {}
view = View(view_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', list(fields.keys())),
])
cls.delete(records)
to_create = []
for field in list(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):
super(ViewTreeState, cls).__register__(module_name)
table = cls.__table_handler__(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), separators=(',', ':'))
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), separators=(',', ':'))
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