merge
This commit is contained in:
@@ -1,4 +1,9 @@
|
||||
* Remove use of python code in workflow XML
|
||||
* Use SQL sequence on PostgreSQL for ir.sequence
|
||||
* Allow to specify more than one interface
|
||||
* Use unoconv for report format conversion
|
||||
* Allow to use any Opendocument as report template
|
||||
* Drop NetRPC and activate JSON-RPC by default
|
||||
* Remove ir.action.wizard_size
|
||||
* Rename expand and fill attributes into yexpand and yfill
|
||||
* Add xalign and yalign as field attributes
|
||||
|
||||
@@ -2,6 +2,7 @@ Copyright (C) 2004-2008 Tiny SPRL.
|
||||
Copyright (C) 2007-2011 Cédric Krier.
|
||||
Copyright (C) 2007-2011 Bertrand Chenal.
|
||||
Copyright (C) 2008-2011 B2CK SPRL.
|
||||
Copyright (C) 2011 Openlabs Technologies & Consulting (P) Ltd.
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -17,7 +17,7 @@ Prerequisites
|
||||
* Optional: pywebdav 0.9.3 or later (http://code.google.com/p/pywebdav/)
|
||||
* Optional: pydot (http://code.google.com/p/pydot/)
|
||||
* Optional: pytz (http://pytz.sourceforge.net/)
|
||||
* Optional: openoffice-python (http://pypi.python.org/pypi/openoffice-python/)
|
||||
* Optional: unoconv http://dag.wieers.com/home-made/unoconv/)
|
||||
* Optional: sphinx (http://sphinx.pocoo.org/)
|
||||
|
||||
Install Tryton
|
||||
|
||||
@@ -74,6 +74,8 @@ When defining an `ir.action.report` the following attributes are available:
|
||||
example, my_module/my_style.odt. If no style is to be used this field
|
||||
will be blank.
|
||||
|
||||
* ``template_extension``: The template format.
|
||||
|
||||
|
||||
Report Usage
|
||||
============
|
||||
@@ -211,6 +213,7 @@ Then you must activate the new invoice report that exists in your new module:
|
||||
<field name="model">account.invoice</field>
|
||||
<field name="report">my_module/invoice.odt</field>
|
||||
<field name="style">module_name/header_A4.odt</field>
|
||||
<field name="template_extension">odt</field>
|
||||
</record>
|
||||
|
||||
Passing custom data to a report
|
||||
|
||||
+13
-29
@@ -3,33 +3,22 @@
|
||||
[options]
|
||||
|
||||
# This is the hostname used when generating tryton URI
|
||||
#hostname =
|
||||
|
||||
# Configure the IP address for trytond
|
||||
# If empty, the server will listen on all available interfaces
|
||||
#interface =
|
||||
|
||||
# Activate the netrpc protocol
|
||||
#netrpc = True
|
||||
# Configure the port for the netrpc protocol to listen
|
||||
#netport = 8070
|
||||
|
||||
# Activate the xml-rpc protocol
|
||||
#xmlrpc = False
|
||||
# Configure the port for the xml-rpc protocol to listen
|
||||
#xmlport = 8069
|
||||
#hostname =
|
||||
|
||||
# Activate the json-rpc protocol
|
||||
#jsonrpc = False
|
||||
# Configure the port for the json-rpc protocol to listen
|
||||
#jsonport = 8000
|
||||
jsonrpc = localhost:8000
|
||||
#ssl_jsonrpc = False
|
||||
|
||||
# Configure the path of json-rpc data
|
||||
#jsondata_path = /var/www/localhost/tryton
|
||||
|
||||
# Activate the xml-rpc protocol
|
||||
#xmlrpc = *:8069
|
||||
#ssl_xmlrpc = False
|
||||
|
||||
# Activate the webdav protocol
|
||||
#webdav = False
|
||||
# Configure the port for the webdav protocol to listen
|
||||
#webdavport = 8080
|
||||
#webdav = *:8080
|
||||
#ssl_webdav = False
|
||||
|
||||
# Configure the database type
|
||||
# allowed values are postgresql, sqlite, mysql
|
||||
@@ -56,11 +45,6 @@
|
||||
#pidfile = False
|
||||
#logfile = False
|
||||
|
||||
# Enable secure connection with SSL
|
||||
#secure_netrpc = False
|
||||
#secure_xmlrpc = False
|
||||
#secure_jsonrpc = False
|
||||
#secure_webdav = False
|
||||
#privatekey = server.pem
|
||||
#certificate = server.pem
|
||||
|
||||
@@ -75,9 +59,6 @@
|
||||
# Configure the path to store attachments and sqlite database
|
||||
#data_path = /var/lib/trytond
|
||||
|
||||
# Limit the number of threads
|
||||
#max_thread = 40
|
||||
|
||||
# Allow to run more than one instance of trytond
|
||||
#multi_server = False
|
||||
|
||||
@@ -96,3 +77,6 @@
|
||||
|
||||
# Enable cron
|
||||
# cron = True
|
||||
|
||||
# unoconv connection
|
||||
#unoconv = pipe,name=trytond;urp;StarOffice.ComponentContext
|
||||
|
||||
@@ -63,7 +63,7 @@ setup(name=PACKAGE,
|
||||
'PostgreSQL': ['psycopg2 >= 2.0'],
|
||||
'MySQL': ['MySQL-python'],
|
||||
'WebDAV': ['PyWebDAV >= 0.9.3'],
|
||||
'PDF': ['openoffice-python'],
|
||||
'unoconv': ['unoconv'],
|
||||
'SSL': ['pyOpenSSL'],
|
||||
'graphviz': ['pydot'],
|
||||
'timezone': ['pytz'],
|
||||
|
||||
+28
-16
@@ -2,21 +2,36 @@
|
||||
#this repository contains the full copyright notices and license terms.
|
||||
import ConfigParser, os, sys
|
||||
|
||||
def get_hostname(netloc):
|
||||
if '[' in netloc and ']' in netloc:
|
||||
return netloc.split(']')[0][1:]
|
||||
elif ':' in netloc:
|
||||
return netloc.split(':')[0]
|
||||
else:
|
||||
return netloc
|
||||
|
||||
def get_port(netloc, protocol):
|
||||
netloc = netloc.split(']')[-1]
|
||||
if ':' in netloc:
|
||||
return int(netloc.split(':')[1])
|
||||
else:
|
||||
return {
|
||||
'jsonrpc': 8000,
|
||||
'xmlrpc': 8069,
|
||||
'webdav': 8080,
|
||||
}.get(protocol)
|
||||
|
||||
class ConfigManager(object):
|
||||
def __init__(self, fname=None):
|
||||
self.options = {
|
||||
'hostname': None,
|
||||
'interface': '',
|
||||
'netrpc': True,
|
||||
'netport': 8070,
|
||||
'xmlrpc': False,
|
||||
'xmlport': 8069,
|
||||
'jsonrpc': False,
|
||||
'jsonport': 8000,
|
||||
'jsonrpc': [('localhost', '8000')],
|
||||
'ssl_jsonrpc': False,
|
||||
'xmlrpc': [],
|
||||
'ssl_xmlrpc': False,
|
||||
'jsondata_path': '/var/www/localhost/tryton',
|
||||
'webdav': False,
|
||||
'webdavport': 8080,
|
||||
'webdav': [],
|
||||
'ssl_webdav': False,
|
||||
'db_type': 'postgresql',
|
||||
'db_host': False,
|
||||
'db_port': False,
|
||||
@@ -31,10 +46,6 @@ class ConfigManager(object):
|
||||
'debug_mode': False,
|
||||
'pidfile': None,
|
||||
'logfile': None,
|
||||
'secure_netrpc': False,
|
||||
'secure_xmlrpc': False,
|
||||
'secure_jsonrpc': False,
|
||||
'secure_webdav': False,
|
||||
'privatekey': '/etc/ssl/trytond/server.key',
|
||||
'certificate': '/etc/ssl/trytond/server.pem',
|
||||
'smtp_server': 'localhost',
|
||||
@@ -44,7 +55,6 @@ class ConfigManager(object):
|
||||
'smtp_user': False,
|
||||
'smtp_password': False,
|
||||
'data_path': '/var/lib/trytond',
|
||||
'max_thread': 40,
|
||||
'multi_server': False,
|
||||
'session_timeout': 600,
|
||||
'psyco': False,
|
||||
@@ -53,6 +63,7 @@ class ConfigManager(object):
|
||||
'init': {},
|
||||
'update': {},
|
||||
'cron': True,
|
||||
'unoconv': 'pipe,name=trytond;urp;StarOffice.ComponentContext',
|
||||
}
|
||||
self.configfile = None
|
||||
|
||||
@@ -92,8 +103,9 @@ class ConfigManager(object):
|
||||
value = True
|
||||
if value == 'False' or value == 'false':
|
||||
value = False
|
||||
if name in ('netport', 'xmlport', 'webdavport', 'jsonport'):
|
||||
value = int(value)
|
||||
if name in ('netrpc', 'xmlrpc', 'jsonrpc', 'webdav'):
|
||||
value = [(get_hostname(netloc).replace('*', ''),
|
||||
get_port(netloc, name)) for netloc in value.split(',')]
|
||||
self.options[name] = value
|
||||
|
||||
def get(self, key, default=None):
|
||||
|
||||
+80
-5
@@ -226,10 +226,76 @@ class ActionReport(ModelSQL, ModelView):
|
||||
style_content = fields.Function(fields.Binary('Style'),
|
||||
'get_style_content')
|
||||
direct_print = fields.Boolean('Direct Print')
|
||||
extension = fields.Selection(
|
||||
[('odt', 'ODT Document'),
|
||||
('pdf', 'PDF Document'),],
|
||||
string='Extension', required=True)
|
||||
template_extension = fields.Selection([
|
||||
('odt', 'OpenDocument Text'),
|
||||
('odp', 'OpenDocument Presentation'),
|
||||
('ods', 'OpenDocument Spreadsheet'),
|
||||
('odg', 'OpenDocument Graphics'),
|
||||
], string='Template Extension', required=True)
|
||||
extension = fields.Selection([
|
||||
('', ''),
|
||||
('bib', 'BibTex'),
|
||||
('bmp', 'Windows Bitmap'),
|
||||
('csv', 'Text CSV'),
|
||||
('dbf', 'dBase'),
|
||||
('dif', 'Data Interchange Format'),
|
||||
('doc', 'Microsoft Word 97/2000/XP'),
|
||||
('doc6', 'Microsoft Word 6.0'),
|
||||
('doc95', 'Microsoft Word 95'),
|
||||
('docbook', 'DocBook'),
|
||||
('emf', 'Enhanced Metafile'),
|
||||
('eps', 'Encapsulated PostScript'),
|
||||
('gif', 'Graphics Interchange Format'),
|
||||
('html', 'HTML Document'),
|
||||
('jpg', 'Joint Photographic Experts Group'),
|
||||
('met', 'OS/2 Metafile'),
|
||||
('ooxml', 'Microsoft Office Open XML'),
|
||||
('pbm', 'Portable Bitmap'),
|
||||
('pct', 'Mac Pict'),
|
||||
('pdb', 'AportisDoc (Palm)'),
|
||||
('pdf', 'Portable Document Format'),
|
||||
('pgm', 'Portable Graymap'),
|
||||
('png', 'Portable Network Graphic'),
|
||||
('ppm', 'Portable Pixelmap'),
|
||||
('ppt', 'Microsoft PowerPoint 97/2000/XP'),
|
||||
('psw', 'Pocket Word'),
|
||||
('pwp', 'PlaceWare'),
|
||||
('pxl', 'Pocket Excel'),
|
||||
('ras', 'Sun Raster Image'),
|
||||
('rtf', 'Rich Text Format'),
|
||||
('latex', 'LaTeX 2e'),
|
||||
('sda', 'StarDraw 5.0 (OpenOffice.org Impress)'),
|
||||
('sdc', 'StarCalc 5.0'),
|
||||
('sdc4', 'StarCalc 4.0'),
|
||||
('sdc3', 'StarCalc 3.0'),
|
||||
('sdd', 'StarImpress 5.0'),
|
||||
('sdd3', 'StarDraw 3.0 (OpenOffice.org Impress)'),
|
||||
('sdd4', 'StarImpress 4.0'),
|
||||
('sdw', 'StarWriter 5.0'),
|
||||
('sdw4', 'StarWriter 4.0'),
|
||||
('sdw3', 'StarWriter 3.0'),
|
||||
('slk', 'SYLK'),
|
||||
('svg', 'Scalable Vector Graphics'),
|
||||
('svm', 'StarView Metafile'),
|
||||
('swf', 'Macromedia Flash (SWF)'),
|
||||
('sxc', 'OpenOffice.org 1.0 Spreadsheet'),
|
||||
('sxi', 'OpenOffice.org 1.0 Presentation'),
|
||||
('sxd', 'OpenOffice.org 1.0 Drawing'),
|
||||
('sxd3', 'StarDraw 3.0'),
|
||||
('sxd5', 'StarDraw 5.0'),
|
||||
('sxw', 'Open Office.org 1.0 Text Document'),
|
||||
('text', 'Text Encoded'),
|
||||
('tiff', 'Tagged Image File Format'),
|
||||
('txt', 'Plain Text'),
|
||||
('wmf', 'Windows Metafile'),
|
||||
('xhtml', 'XHTML Document'),
|
||||
('xls', 'Microsoft Excel 97/2000/XP'),
|
||||
('xls5', 'Microsoft Excel 5.0'),
|
||||
('xls95', 'Microsoft Excel 95'),
|
||||
('xpm', 'X PixMap'),
|
||||
],
|
||||
string='Extension', help='Leave empty for the same as template, '
|
||||
'see unoconv documentation for compatible format')
|
||||
module = fields.Char('Module', readonly=True, select=1)
|
||||
email = fields.Char('Email')
|
||||
pyson_email = fields.Function(fields.Char('PySON Email'), 'get_pyson')
|
||||
@@ -269,6 +335,12 @@ class ActionReport(ModelSQL, ModelView):
|
||||
TableHandler.dropTable(cursor, 'ir.action.report.outputformat',
|
||||
'ir_action_report_outputformat')
|
||||
|
||||
# Migrate from 2.0 remove required on extension
|
||||
table.not_null_action('extension', action='remove')
|
||||
cursor.execute('UPDATE "' + self._table + '" '
|
||||
'SET extension = %s '
|
||||
'WHERE extension = %s', ('', 'odt'))
|
||||
|
||||
def default_type(self):
|
||||
return 'ir.action.report'
|
||||
|
||||
@@ -278,9 +350,12 @@ class ActionReport(ModelSQL, ModelView):
|
||||
def default_direct_print(self):
|
||||
return False
|
||||
|
||||
def default_extension(self):
|
||||
def default_template_extension(self):
|
||||
return 'odt'
|
||||
|
||||
def default_extension(self):
|
||||
return ''
|
||||
|
||||
def default_module(self):
|
||||
return Transaction().context.get('module') or ''
|
||||
|
||||
|
||||
@@ -92,9 +92,11 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<field name="icon"/>
|
||||
<newline/>
|
||||
<label name="report"/>
|
||||
<field name="report" colspan="3"/>
|
||||
<field name="report"/>
|
||||
<label name="report_content"/>
|
||||
<field name="report_content"/>
|
||||
<label name="template_extension"/>
|
||||
<field name="template_extension"/>
|
||||
<label name="extension"/>
|
||||
<field name="extension"/>
|
||||
<label name="style"/>
|
||||
|
||||
+130
-7
@@ -8,6 +8,10 @@ from trytond.tools import datetime_strftime
|
||||
from trytond.pyson import In, Eval, Not
|
||||
from trytond.transaction import Transaction
|
||||
from trytond.pool import Pool
|
||||
from trytond.config import CONFIG
|
||||
from trytond.backend import TableHandler
|
||||
|
||||
sql_sequence = CONFIG.options['db_type'] == 'postgresql'
|
||||
|
||||
|
||||
class SequenceType(ModelSQL, ModelView):
|
||||
@@ -24,6 +28,7 @@ class Sequence(ModelSQL, ModelView):
|
||||
"Sequence"
|
||||
_name = 'ir.sequence'
|
||||
_description = __doc__
|
||||
_strict = False
|
||||
name = fields.Char('Sequence Name', required=True, translate=True)
|
||||
code = fields.Selection('code_get', 'Sequence Code', required=True,
|
||||
states={
|
||||
@@ -37,10 +42,12 @@ class Sequence(ModelSQL, ModelView):
|
||||
('decimal timestamp', 'Decimal Timestamp'),
|
||||
('hexadecimal timestamp', 'Hexadecimal Timestamp'),
|
||||
], 'Type')
|
||||
number_next = fields.Integer('Next Number',
|
||||
number_next_internal = fields.Integer('Next Number',
|
||||
states={
|
||||
'invisible': Not(In(Eval('type'), ['incremental'])),
|
||||
}, depends=['type'])
|
||||
number_next = fields.Function(number_next_internal, 'get_number_next',
|
||||
'set_number_next')
|
||||
number_increment = fields.Integer('Increment Number',
|
||||
states={
|
||||
'invisible': Not(In(Eval('type'), ['incremental'])),
|
||||
@@ -77,6 +84,26 @@ class Sequence(ModelSQL, ModelView):
|
||||
'future_last_timestamp': 'Last Timestamp could not be in future!',
|
||||
})
|
||||
|
||||
def init(self, module_name):
|
||||
cursor = Transaction().cursor
|
||||
table = TableHandler(cursor, self, module_name)
|
||||
|
||||
# Migration from 2.0 rename number_next into number_next_internal
|
||||
table.column_rename('number_next', 'number_next_internal')
|
||||
|
||||
super(Sequence, self).init(module_name)
|
||||
|
||||
# Migration from 2.0 create sql_sequence
|
||||
if sql_sequence and not self._strict:
|
||||
sequence_ids = self.search([])
|
||||
for sequence in self.browse(sequence_ids):
|
||||
if sequence.type != 'incremental':
|
||||
continue
|
||||
if not TableHandler.sequence_exist(cursor,
|
||||
self._sql_sequence_name(sequence)):
|
||||
self.create_sql_sequence(sequence,
|
||||
sequence.number_next_internal)
|
||||
|
||||
def default_active(self):
|
||||
return True
|
||||
|
||||
@@ -104,6 +131,53 @@ class Sequence(ModelSQL, ModelView):
|
||||
def default_code(self):
|
||||
return Transaction().context.get('code', False)
|
||||
|
||||
def get_number_next(self, ids, name):
|
||||
cursor = Transaction().cursor
|
||||
result = {}
|
||||
for sequence in self.browse(ids):
|
||||
sql_name = self._sql_sequence_name(sequence)
|
||||
if sql_sequence and not self._strict:
|
||||
cursor.execute('SELECT '
|
||||
'CASE WHEN NOT is_called THEN last_value '
|
||||
'ELSE last_value + increment_by '
|
||||
'END FROM "%s"' % sql_name)
|
||||
value, = cursor.fetchone()
|
||||
else:
|
||||
value = sequence.number_next_internal
|
||||
result[sequence.id] = value
|
||||
return result
|
||||
|
||||
def set_number_next(self, ids, name, value):
|
||||
cursor = Transaction().cursor
|
||||
super(Sequence, self).write(ids, {
|
||||
'number_next_internal': value,
|
||||
})
|
||||
|
||||
def create(self, values):
|
||||
sequence_id = super(Sequence, self).create(values)
|
||||
if sql_sequence and not self._strict:
|
||||
sequence = self.browse(sequence_id)
|
||||
self.update_sql_sequence(sequence, values.get('number_next',
|
||||
self.default_number_next()))
|
||||
return sequence_id
|
||||
|
||||
def write(self, ids, values):
|
||||
result = super(Sequence, self).write(ids, values)
|
||||
if sql_sequence and not self._strict:
|
||||
ids = [ids] if isinstance(ids, (int, long)) else ids
|
||||
sequences = self.browse(ids)
|
||||
for sequence in sequences:
|
||||
self.update_sql_sequence(sequence, values.get('number_next'))
|
||||
return result
|
||||
|
||||
def delete(self, ids):
|
||||
if sql_sequence and not self._strict:
|
||||
ids = [ids] if isinstance(ids, (int, long)) else ids
|
||||
sequences = self.browse(ids)
|
||||
for sequence in sequences:
|
||||
self.delete_sql_sequence(sequence)
|
||||
return super(Sequence, self).delete(ids)
|
||||
|
||||
def code_get(self):
|
||||
pool = Pool()
|
||||
sequence_type_obj = pool.get('ir.sequence.type')
|
||||
@@ -131,6 +205,47 @@ class Sequence(ModelSQL, ModelView):
|
||||
return False
|
||||
return True
|
||||
|
||||
def _sql_sequence_name(self, sequence):
|
||||
'Return SQL sequence name'
|
||||
return '%s_%s' % (self._table, sequence.id)
|
||||
|
||||
def create_sql_sequence(self, sequence, number_next=None):
|
||||
'Create the SQL sequence'
|
||||
cursor = Transaction().cursor
|
||||
if sequence.type != 'incremental':
|
||||
return
|
||||
if number_next is None:
|
||||
number_next = sequence.number_next
|
||||
cursor.execute('CREATE SEQUENCE "' + self._sql_sequence_name(sequence)
|
||||
+ '" INCREMENT BY %s START WITH %s', (sequence.number_increment,
|
||||
number_next))
|
||||
|
||||
def update_sql_sequence(self, sequence, number_next=None):
|
||||
'Update the SQL sequence'
|
||||
cursor = Transaction().cursor
|
||||
exist = TableHandler.sequence_exist(cursor,
|
||||
self._sql_sequence_name(sequence))
|
||||
if sequence.type != 'incremental':
|
||||
if exist:
|
||||
self.delete_sql_sequence(sequence)
|
||||
return
|
||||
if not exist:
|
||||
self.create_sql_sequence(sequence, number_next)
|
||||
return
|
||||
if number_next is None:
|
||||
number_next = sequence.number_next
|
||||
cursor.execute('ALTER SEQUENCE "' + self._sql_sequence_name(sequence)
|
||||
+ '" INCREMENT BY %s RESTART WITH %s', (sequence.number_increment,
|
||||
number_next))
|
||||
|
||||
def delete_sql_sequence(self, sequence):
|
||||
'Delete the SQL sequence'
|
||||
cursor = Transaction().cursor
|
||||
if sequence.type != 'incremental':
|
||||
return
|
||||
cursor.execute('DROP SEQUENCE "%s"'
|
||||
% self._sql_sequence_name(sequence))
|
||||
|
||||
def _process(self, string, date=None):
|
||||
pool = Pool()
|
||||
date_obj = pool.get('ir.date')
|
||||
@@ -151,13 +266,20 @@ class Sequence(ModelSQL, ModelView):
|
||||
|
||||
def _get_sequence(self, sequence):
|
||||
if sequence.type == 'incremental':
|
||||
#Pre-fetch number_next
|
||||
number_next = sequence.number_next or 0
|
||||
if sql_sequence and not self._strict:
|
||||
cursor = Transaction().cursor
|
||||
cursor.execute('SELECT nextval(\'"%s"\')'
|
||||
% self._sql_sequence_name(sequence))
|
||||
number_next, = cursor.fetchone()
|
||||
else:
|
||||
#Pre-fetch number_next
|
||||
number_next = sequence.number_next_internal
|
||||
|
||||
with Transaction().set_user(0):
|
||||
self.write(sequence.id, {
|
||||
'number_next': number_next + sequence.number_increment,
|
||||
})
|
||||
with Transaction().set_user(0):
|
||||
self.write(sequence.id, {
|
||||
'number_next_internal': (number_next
|
||||
+ sequence.number_increment),
|
||||
})
|
||||
return '%%0%sd' % sequence.padding % number_next
|
||||
elif sequence.type in ('decimal timestamp', 'hexadecimal timestamp'):
|
||||
timestamp = sequence.last_timestamp
|
||||
@@ -209,6 +331,7 @@ class SequenceStrict(Sequence):
|
||||
"Sequence Strict"
|
||||
_name = 'ir.sequence.strict'
|
||||
_description = __doc__
|
||||
_strict = True
|
||||
|
||||
def get_id(self, clause):
|
||||
Transaction().cursor.lock(self._table)
|
||||
|
||||
+11
-10
@@ -20,8 +20,8 @@ image = element image { attlist.image, empty }
|
||||
attlist.image &= attribute name { text }
|
||||
attlist.image &= [ a:defaultValue = "1" ] attribute colspan { text }?
|
||||
attlist.image &=
|
||||
[ a:defaultValue = "0" ] attribute expand { "0" | "1" }?
|
||||
attlist.image &= [ a:defaultValue = "0" ] attribute fill { "0" | "1" }?
|
||||
[ a:defaultValue = "0" ] attribute yexpand { "0" | "1" }?
|
||||
attlist.image &= [ a:defaultValue = "0" ] attribute yfill { "0" | "1" }?
|
||||
attlist.image &= attribute help { text }?
|
||||
attlist.image &= attribute states { text }?
|
||||
separator = element separator { attlist.separator, empty }
|
||||
@@ -29,22 +29,23 @@ attlist.separator &= [ a:defaultValue = "" ] attribute string { text }?
|
||||
attlist.separator &= ( attribute name { text } | attribute name { text } )
|
||||
attlist.separator &= attribute states { text }?
|
||||
attlist.separator &=
|
||||
[ a:defaultValue = "0.0" ] attribute align { text }?
|
||||
[ a:defaultValue = "0.0" ] attribute xalign { text }?
|
||||
attlist.separator &=
|
||||
[ a:defaultValue = "1" ] attribute colspan { text }?
|
||||
attlist.separator &=
|
||||
[ a:defaultValue = "0" ] attribute expand { "0" | "1" }?
|
||||
[ a:defaultValue = "0" ] attribute yexpand { "0" | "1" }?
|
||||
attlist.separator &=
|
||||
[ a:defaultValue = "0" ] attribute fill { "0" | "1" }?
|
||||
[ a:defaultValue = "0" ] attribute yfill { "0" | "1" }?
|
||||
attlist.separator &= attribute help { text }?
|
||||
label = element label { attlist.label, empty }
|
||||
attlist.label &= [ a:defaultValue = "" ] attribute string { text }?
|
||||
attlist.label &= ( attribute name { text } | attribute name { text } )
|
||||
attlist.label &= attribute states { text }?
|
||||
attlist.label &= [ a:defaultValue = "0.0" ] attribute align { text }?
|
||||
attlist.label &= [ a:defaultValue = "0.0" ] attribute xalign { text }?
|
||||
attlist.label &= [ a:defaultValue = "0.5" ] attribute yalign { text }?
|
||||
attlist.label &=
|
||||
[ a:defaultValue = "0" ] attribute expand { "0" | "1" }?
|
||||
attlist.label &= [ a:defaultValue = "0" ] attribute fill { "0" | "1" }?
|
||||
[ a:defaultValue = "0" ] attribute yexpand { "0" | "1" }?
|
||||
attlist.label &= [ a:defaultValue = "0" ] attribute yfill { "0" | "1" }?
|
||||
attlist.label &=
|
||||
[ a:defaultValue = "0" ] attribute xexpand { "0" | "1" }?
|
||||
attlist.label &= [ a:defaultValue = "1" ] attribute xfill { "0" | "1" }?
|
||||
@@ -90,8 +91,8 @@ group =
|
||||
attlist.group &= attribute string { text }?
|
||||
attlist.group &= [ a:defaultValue = "1" ] attribute colspan { text }?
|
||||
attlist.group &=
|
||||
[ a:defaultValue = "0" ] attribute expand { "0" | "1" }?
|
||||
attlist.group &= [ a:defaultValue = "1" ] attribute fill { "0" | "1" }?
|
||||
[ a:defaultValue = "0" ] attribute yexpand { "0" | "1" }?
|
||||
attlist.group &= [ a:defaultValue = "1" ] attribute yfill { "0" | "1" }?
|
||||
attlist.group &= [ a:defaultValue = "1" ] attribute rowspan { text }?
|
||||
attlist.group &= [ a:defaultValue = "4" ] attribute col { text }?
|
||||
attlist.group &= attribute id { text }
|
||||
|
||||
+15
-10
@@ -44,7 +44,7 @@
|
||||
</define>
|
||||
<define name="attlist.image" combine="interleave">
|
||||
<optional>
|
||||
<attribute name="expand" a:defaultValue="0">
|
||||
<attribute name="yexpand" a:defaultValue="0">
|
||||
<choice>
|
||||
<value>0</value>
|
||||
<value>1</value>
|
||||
@@ -54,7 +54,7 @@
|
||||
</define>
|
||||
<define name="attlist.image" combine="interleave">
|
||||
<optional>
|
||||
<attribute name="fill" a:defaultValue="0">
|
||||
<attribute name="yfill" a:defaultValue="0">
|
||||
<choice>
|
||||
<value>0</value>
|
||||
<value>1</value>
|
||||
@@ -96,7 +96,7 @@
|
||||
</define>
|
||||
<define name="attlist.separator" combine="interleave">
|
||||
<optional>
|
||||
<attribute name="align" a:defaultValue="0.0"/>
|
||||
<attribute name="xalign" a:defaultValue="0.0"/>
|
||||
</optional>
|
||||
</define>
|
||||
<define name="attlist.separator" combine="interleave">
|
||||
@@ -106,7 +106,7 @@
|
||||
</define>
|
||||
<define name="attlist.separator" combine="interleave">
|
||||
<optional>
|
||||
<attribute name="expand" a:defaultValue="0">
|
||||
<attribute name="yexpand" a:defaultValue="0">
|
||||
<choice>
|
||||
<value>0</value>
|
||||
<value>1</value>
|
||||
@@ -116,7 +116,7 @@
|
||||
</define>
|
||||
<define name="attlist.separator" combine="interleave">
|
||||
<optional>
|
||||
<attribute name="fill" a:defaultValue="0">
|
||||
<attribute name="yfill" a:defaultValue="0">
|
||||
<choice>
|
||||
<value>0</value>
|
||||
<value>1</value>
|
||||
@@ -153,12 +153,17 @@
|
||||
</define>
|
||||
<define name="attlist.label" combine="interleave">
|
||||
<optional>
|
||||
<attribute name="align" a:defaultValue="0.0"/>
|
||||
<attribute name="xalign" a:defaultValue="0.0"/>
|
||||
</optional>
|
||||
</define>
|
||||
<define name="attlist.label" combine="interleave">
|
||||
<optional>
|
||||
<attribute name="expand" a:defaultValue="0">
|
||||
<attribute name="yalign" a:defaultValue="0.5"/>
|
||||
</optional>
|
||||
</define>
|
||||
<define name="attlist.label" combine="interleave">
|
||||
<optional>
|
||||
<attribute name="yexpand" a:defaultValue="0">
|
||||
<choice>
|
||||
<value>0</value>
|
||||
<value>1</value>
|
||||
@@ -168,7 +173,7 @@
|
||||
</define>
|
||||
<define name="attlist.label" combine="interleave">
|
||||
<optional>
|
||||
<attribute name="fill" a:defaultValue="0">
|
||||
<attribute name="yfill" a:defaultValue="0">
|
||||
<choice>
|
||||
<value>0</value>
|
||||
<value>1</value>
|
||||
@@ -309,7 +314,7 @@
|
||||
</define>
|
||||
<define name="attlist.group" combine="interleave">
|
||||
<optional>
|
||||
<attribute name="expand" a:defaultValue="0">
|
||||
<attribute name="yexpand" a:defaultValue="0">
|
||||
<choice>
|
||||
<value>0</value>
|
||||
<value>1</value>
|
||||
@@ -319,7 +324,7 @@
|
||||
</define>
|
||||
<define name="attlist.group" combine="interleave">
|
||||
<optional>
|
||||
<attribute name="fill" a:defaultValue="1">
|
||||
<attribute name="yfill" a:defaultValue="1">
|
||||
<choice>
|
||||
<value>0</value>
|
||||
<value>1</value>
|
||||
|
||||
@@ -32,11 +32,11 @@ class View(ModelSQL, ModelView):
|
||||
ondelete='CASCADE')
|
||||
field_childs = fields.Char('Children Field', states={
|
||||
'invisible': Not(Equal(Eval('type'), 'tree')),
|
||||
})
|
||||
}, depends=['type'])
|
||||
module = fields.Char('Module', readonly=True)
|
||||
domain = fields.Char('Domain', states={
|
||||
'invisible': Not(Bool(Eval('inherit'))),
|
||||
})
|
||||
}, depends=['inherit'])
|
||||
|
||||
def __init__(self):
|
||||
super(View, self).__init__()
|
||||
|
||||
+101
-11
@@ -4,6 +4,13 @@ import errno
|
||||
import os
|
||||
import socket
|
||||
import threading
|
||||
import select
|
||||
import sys
|
||||
import gzip
|
||||
try:
|
||||
import cStringIO as StringIO
|
||||
except ImportError:
|
||||
import StringIO
|
||||
|
||||
def endsocket(sock):
|
||||
if os.name != 'nt':
|
||||
@@ -17,21 +24,104 @@ class daemon(threading.Thread):
|
||||
def __init__(self, interface, port, secure, name=None):
|
||||
threading.Thread.__init__(self, name=name)
|
||||
self.secure = secure
|
||||
self.running = False
|
||||
self.ipv6 = False
|
||||
if socket.has_ipv6:
|
||||
try:
|
||||
socket.getaddrinfo(interface or None, port, socket.AF_INET6)
|
||||
for family, _, _, _, _ in socket.getaddrinfo(interface or None, port,
|
||||
socket.AF_UNSPEC, socket.SOCK_STREAM):
|
||||
if family == socket.AF_INET6:
|
||||
self.ipv6 = True
|
||||
except Exception:
|
||||
pass
|
||||
break
|
||||
|
||||
def stop(self):
|
||||
self.running = False
|
||||
self.server.shutdown()
|
||||
return
|
||||
|
||||
def run(self):
|
||||
self.running = True
|
||||
while self.running:
|
||||
self.server.handle_request()
|
||||
endsocket(self.server.socket)
|
||||
self.server.serve_forever()
|
||||
return True
|
||||
|
||||
class GZipRequestHandlerMixin:
|
||||
|
||||
if sys.version_info[:2] <= (2, 6):
|
||||
# Copy from SimpleXMLRPCServer.py with gzip encoding added
|
||||
def do_POST(self):
|
||||
"""Handles the HTTP POST request.
|
||||
|
||||
Attempts to interpret all HTTP POST requests as XML-RPC calls,
|
||||
which are forwarded to the server's _dispatch method for handling.
|
||||
"""
|
||||
|
||||
# Check that the path is legal
|
||||
if not self.is_rpc_path_valid():
|
||||
self.report_404()
|
||||
return
|
||||
|
||||
try:
|
||||
# Get arguments by reading body of request.
|
||||
# We read this in chunks to avoid straining
|
||||
# socket.read(); around the 10 or 15Mb mark, some platforms
|
||||
# begin to have problems (bug #792570).
|
||||
max_chunk_size = 10*1024*1024
|
||||
size_remaining = int(self.headers["content-length"])
|
||||
L = []
|
||||
while size_remaining:
|
||||
chunk_size = min(size_remaining, max_chunk_size)
|
||||
L.append(self.rfile.read(chunk_size))
|
||||
size_remaining -= len(L[-1])
|
||||
data = ''.join(L)
|
||||
|
||||
data = self.decode_request_content(data)
|
||||
if data is None:
|
||||
return #response has been sent
|
||||
|
||||
# In previous versions of SimpleXMLRPCServer, _dispatch
|
||||
# could be overridden in this class, instead of in
|
||||
# SimpleXMLRPCDispatcher. To maintain backwards compatibility,
|
||||
# check to see if a subclass implements _dispatch and dispatch
|
||||
# using that method if present.
|
||||
response = self.server._marshaled_dispatch(
|
||||
data, getattr(self, '_dispatch', None)
|
||||
)
|
||||
except Exception: # This should only happen if the module is buggy
|
||||
# internal error, report as HTTP server error
|
||||
self.send_response(500)
|
||||
self.end_headers()
|
||||
else:
|
||||
# got a valid XML RPC response
|
||||
self.send_response(200)
|
||||
self.send_header("Content-type", "text/xml")
|
||||
|
||||
# Handle gzip encoding
|
||||
if 'gzip' in self.headers.get('Accept-Encoding', '').split(',') \
|
||||
and len(response) > self.encode_threshold:
|
||||
buffer = StringIO.StringIO()
|
||||
output = gzip.GzipFile(mode='wb', fileobj=buffer)
|
||||
output.write(response)
|
||||
output.close()
|
||||
buffer.seek(0)
|
||||
response = buffer.getvalue()
|
||||
self.send_header('Content-Encoding', 'gzip')
|
||||
|
||||
self.send_header("Content-length", str(len(response)))
|
||||
self.end_headers()
|
||||
self.wfile.write(response)
|
||||
|
||||
def decode_request_content(self, data):
|
||||
#support gzip encoding of request
|
||||
encoding = self.headers.get("content-encoding", "identity").lower()
|
||||
if encoding == "identity":
|
||||
return data
|
||||
if encoding == "gzip":
|
||||
f = StringIO.StringIO(data)
|
||||
gzf = gzip.GzipFile(mode="rb", fileobj=f)
|
||||
try:
|
||||
decoded = gzf.read()
|
||||
except IOError:
|
||||
self.send_response(400, "error decoding gzip content")
|
||||
f.close()
|
||||
gzf.close()
|
||||
return decoded
|
||||
else:
|
||||
self.send_response(501, "encoding %r not supported" % encoding)
|
||||
self.send_header("Content-length", "0")
|
||||
self.end_headers()
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ from trytond.protocols.sslsocket import SSLSocket
|
||||
from trytond.protocols.dispatcher import dispatch
|
||||
from trytond.config import CONFIG
|
||||
from trytond.protocols.datatype import Float
|
||||
from trytond.protocols.common import daemon
|
||||
from trytond.protocols.common import daemon, GZipRequestHandlerMixin
|
||||
import SimpleXMLRPCServer
|
||||
import SimpleHTTPServer
|
||||
import SocketServer
|
||||
@@ -12,8 +12,6 @@ import traceback
|
||||
import socket
|
||||
import sys
|
||||
import os
|
||||
import gzip
|
||||
import StringIO
|
||||
try:
|
||||
import fcntl
|
||||
except ImportError:
|
||||
@@ -31,6 +29,8 @@ def object_hook(dct):
|
||||
dct['hour'], dct['minute'], dct['second'])
|
||||
elif dct['__class__'] == 'date':
|
||||
return datetime.date(dct['year'], dct['month'], dct['day'])
|
||||
elif dct['__class__'] == 'Decimal':
|
||||
return Decimal(dct['decimal'])
|
||||
return dct
|
||||
|
||||
class JSONEncoder(json.JSONEncoder):
|
||||
@@ -52,7 +52,9 @@ class JSONEncoder(json.JSONEncoder):
|
||||
'day': obj.day,
|
||||
}
|
||||
elif isinstance(obj, Decimal):
|
||||
return float(obj)
|
||||
return {'__class__': 'Decimal',
|
||||
'decimal': str(obj),
|
||||
}
|
||||
return super(JSONEncoder, self).default(obj)
|
||||
|
||||
|
||||
@@ -64,7 +66,7 @@ class SimpleJSONRPCDispatcher(SimpleXMLRPCServer.SimpleXMLRPCDispatcher):
|
||||
reason to instantiate this class directly.
|
||||
"""
|
||||
|
||||
def _marshaled_dispatch(self, data, dispatch_method=None):
|
||||
def _marshaled_dispatch(self, data, dispatch_method=None, path=None):
|
||||
"""Dispatches an JSON-RPC method from marshalled (JSON) data.
|
||||
|
||||
JSON-RPC methods are dispatched from the marshalled (JSON) data
|
||||
@@ -75,7 +77,7 @@ class SimpleJSONRPCDispatcher(SimpleXMLRPCServer.SimpleXMLRPCDispatcher):
|
||||
existing method through subclassing is the prefered means
|
||||
of changing method dispatch behavior.
|
||||
"""
|
||||
rawreq = json.loads(data, object_hook=object_hook, parse_float=Float)
|
||||
rawreq = json.loads(data, object_hook=object_hook)
|
||||
|
||||
req_id = rawreq.get('id', 0)
|
||||
method = rawreq['method']
|
||||
@@ -104,7 +106,7 @@ class SimpleJSONRPCDispatcher(SimpleXMLRPCServer.SimpleXMLRPCDispatcher):
|
||||
traceb = sys.exc_info()[2]
|
||||
pdb.post_mortem(traceb)
|
||||
# report exception back to server
|
||||
response['error'] = "%s:\n%s" % (sys.exc_value, tb_s)
|
||||
response['error'] = (str(sys.exc_value), tb_s)
|
||||
|
||||
return json.dumps(response, cls=JSONEncoder)
|
||||
|
||||
@@ -126,7 +128,8 @@ class GenericJSONRPCRequestHandler:
|
||||
return res
|
||||
|
||||
|
||||
class SimpleJSONRPCRequestHandler(GenericJSONRPCRequestHandler,
|
||||
class SimpleJSONRPCRequestHandler(GZipRequestHandlerMixin,
|
||||
GenericJSONRPCRequestHandler,
|
||||
SimpleXMLRPCServer.SimpleXMLRPCRequestHandler,
|
||||
SimpleHTTPServer.SimpleHTTPRequestHandler):
|
||||
"""Simple JSON-RPC request handler class.
|
||||
@@ -134,71 +137,15 @@ class SimpleJSONRPCRequestHandler(GenericJSONRPCRequestHandler,
|
||||
Handles all HTTP POST requests and attempts to decode them as
|
||||
JSON-RPC requests.
|
||||
"""
|
||||
protocol_version = "HTTP/1.1"
|
||||
rpc_paths = None
|
||||
encode_threshold = 1400 # common MTU
|
||||
|
||||
# Copy from SimpleXMLRPCServer.py with gzip encoding added
|
||||
def do_POST(self):
|
||||
"""Handles the HTTP POST request.
|
||||
|
||||
Attempts to interpret all HTTP POST requests as JSON-RPC calls,
|
||||
which are forwarded to the server's _dispatch method for handling.
|
||||
"""
|
||||
|
||||
# Check that the path is legal
|
||||
if not self.is_rpc_path_valid():
|
||||
self.report_404()
|
||||
return
|
||||
|
||||
try:
|
||||
# Get arguments by reading body of request.
|
||||
# We read this in chunks to avoid straining
|
||||
# socket.read(); around the 10 or 15Mb mark, some platforms
|
||||
# begin to have problems (bug #792570).
|
||||
max_chunk_size = 10*1024*1024
|
||||
size_remaining = int(self.headers["content-length"])
|
||||
L = []
|
||||
while size_remaining:
|
||||
chunk_size = min(size_remaining, max_chunk_size)
|
||||
L.append(self.rfile.read(chunk_size))
|
||||
size_remaining -= len(L[-1])
|
||||
data = ''.join(L)
|
||||
|
||||
# In previous versions of SimpleXMLRPCServer, _dispatch
|
||||
# could be overridden in this class, instead of in
|
||||
# SimpleXMLRPCDispatcher. To maintain backwards compatibility,
|
||||
# check to see if a subclass implements _dispatch and dispatch
|
||||
# using that method if present.
|
||||
response = self.server._marshaled_dispatch(
|
||||
data, getattr(self, '_dispatch', None)
|
||||
)
|
||||
except Exception: # This should only happen if the module is buggy
|
||||
# internal error, report as HTTP server error
|
||||
self.send_response(500)
|
||||
self.end_headers()
|
||||
else:
|
||||
# got a valid JSON RPC response
|
||||
self.send_response(200)
|
||||
self.send_header("Content-type", "application/json-rpc")
|
||||
|
||||
# Handle gzip encoding
|
||||
if 'gzip' in self.headers.get('Accept-Encoding', '').split(',') \
|
||||
and len(response) > self.encode_threshold:
|
||||
buffer = StringIO.StringIO()
|
||||
output = gzip.GzipFile(mode='wb', fileobj=buffer)
|
||||
output.write(response)
|
||||
output.close()
|
||||
buffer.seek(0)
|
||||
response = buffer.getvalue()
|
||||
self.send_header('Content-Encoding', 'gzip')
|
||||
|
||||
self.send_header("Content-length", str(len(response)))
|
||||
self.end_headers()
|
||||
self.wfile.write(response)
|
||||
|
||||
# shut down the connection
|
||||
self.wfile.flush()
|
||||
self.connection.shutdown(1)
|
||||
def send_header(self, keyword, value):
|
||||
if keyword == 'Content-type' and value == 'text/xml':
|
||||
value = 'application/json-rpc'
|
||||
SimpleXMLRPCServer.SimpleXMLRPCRequestHandler.send_header(self,
|
||||
keyword, value)
|
||||
|
||||
def translate_path(self, path):
|
||||
"""Translate a /-separated PATH to the local filename syntax.
|
||||
@@ -274,6 +221,7 @@ class SimpleJSONRPCServer(SocketServer.TCPServer,
|
||||
class SimpleThreadedJSONRPCServer(SocketServer.ThreadingMixIn,
|
||||
SimpleJSONRPCServer):
|
||||
timeout = 1
|
||||
daemon_threads = True
|
||||
|
||||
def server_bind(self):
|
||||
self.socket.setsockopt(socket.SOL_SOCKET,
|
||||
@@ -304,7 +252,6 @@ class JSONRPCDaemon(daemon):
|
||||
|
||||
def __init__(self, interface, port, secure=False):
|
||||
daemon.__init__(self, interface, port, secure, name='JSONRPCDaemon')
|
||||
|
||||
if self.secure:
|
||||
handler_class = SecureJSONRPCRequestHandler
|
||||
server_class = SecureThreadedJSONRPCServer
|
||||
|
||||
@@ -1,139 +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 trytond.pysocket import PySocket
|
||||
from trytond.protocols.sslsocket import SSLSocket
|
||||
from trytond.config import CONFIG
|
||||
from trytond.protocols.dispatcher import dispatch
|
||||
from trytond.protocols.common import endsocket
|
||||
import threading
|
||||
import select
|
||||
import traceback
|
||||
import socket
|
||||
import sys
|
||||
|
||||
|
||||
class NetRPCClientThread(threading.Thread):
|
||||
def __init__(self, sock, threads, secure):
|
||||
threading.Thread.__init__(self, name='NetRPCClientThread')
|
||||
self.sock = sock
|
||||
self.threads = threads
|
||||
self.running = False
|
||||
self.secure = secure
|
||||
|
||||
def run(self):
|
||||
self.running = True
|
||||
try:
|
||||
pysocket = PySocket(self.sock)
|
||||
except Exception:
|
||||
self.sock.close()
|
||||
self.threads.remove(self)
|
||||
return False
|
||||
timeout = 0
|
||||
while self.running:
|
||||
try:
|
||||
rlist, _, _ = select.select([self.sock], [], [], 1)
|
||||
except select.error:
|
||||
continue
|
||||
if not rlist:
|
||||
timeout += 1
|
||||
if timeout > 600:
|
||||
break
|
||||
continue
|
||||
timeout = 0
|
||||
try:
|
||||
msg = pysocket.receive()
|
||||
except Exception:
|
||||
pysocket.disconnect()
|
||||
self.threads.remove(self)
|
||||
return False
|
||||
host, port = self.sock.getpeername()[:2]
|
||||
try:
|
||||
res = dispatch(host, port, 'NetRPC', *msg)
|
||||
pysocket.send(res)
|
||||
except Exception, exception:
|
||||
tb_s = ''
|
||||
for line in traceback.format_exception(*sys.exc_info()):
|
||||
try:
|
||||
line = line.encode('utf-8', 'ignore')
|
||||
except Exception:
|
||||
continue
|
||||
tb_s += line
|
||||
for path in sys.path:
|
||||
tb_s = tb_s.replace(path, '')
|
||||
if CONFIG['debug_mode']:
|
||||
import pdb
|
||||
tback = sys.exc_info()[2]
|
||||
pdb.post_mortem(tback)
|
||||
try:
|
||||
pysocket.send(exception.args, exception=True,
|
||||
traceback=tb_s)
|
||||
except Exception:
|
||||
pysocket.disconnect()
|
||||
self.threads.remove(self)
|
||||
return False
|
||||
pysocket.disconnect()
|
||||
self.threads.remove(self)
|
||||
return True
|
||||
|
||||
def stop(self):
|
||||
self.running = False
|
||||
|
||||
|
||||
class NetRPCServerThread(threading.Thread):
|
||||
def __init__(self, interface, port, secure=False):
|
||||
threading.Thread.__init__(self, name='NetRPCServerThread')
|
||||
self.socket = None
|
||||
if socket.has_ipv6:
|
||||
try:
|
||||
socket.getaddrinfo(interface or None, port, socket.AF_INET6)
|
||||
self.socket = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
|
||||
except Exception:
|
||||
pass
|
||||
if self.socket is None:
|
||||
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
if secure:
|
||||
self.socket = SSLSocket(self.socket)
|
||||
self.socket.bind((interface, port))
|
||||
self.socket.listen(5)
|
||||
self.threads = []
|
||||
self.secure = secure
|
||||
self.running = False
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
self.running = True
|
||||
while self.running:
|
||||
if not int(CONFIG['max_thread']) \
|
||||
or len(self.threads) < int(CONFIG['max_thread']):
|
||||
try:
|
||||
rlist, _, _ = select.select([self.socket], [], [], 1)
|
||||
except select.error:
|
||||
continue
|
||||
if not rlist:
|
||||
continue
|
||||
(clientsocket, _) = self.socket.accept()
|
||||
c_thread = NetRPCClientThread(clientsocket, self.threads,
|
||||
self.secure)
|
||||
self.threads.append(c_thread)
|
||||
c_thread.start()
|
||||
except Exception:
|
||||
try:
|
||||
self.socket.close()
|
||||
except Exception:
|
||||
pass
|
||||
return False
|
||||
|
||||
def stop(self):
|
||||
self.running = False
|
||||
while len(self.threads):
|
||||
try:
|
||||
thread = self.threads[0]
|
||||
thread.stop()
|
||||
thread.join()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
endsocket(self.socket)
|
||||
except Exception:
|
||||
return False
|
||||
@@ -78,7 +78,6 @@ if not hasattr(WebDAVServer.DAVRequestHandler, '_config'):
|
||||
class BaseThreadedHTTPServer(SocketServer.ThreadingMixIn,
|
||||
BaseHTTPServer.HTTPServer):
|
||||
timeout = 1
|
||||
max_children = CONFIG['max_thread']
|
||||
|
||||
def server_bind(self):
|
||||
self.socket.setsockopt(socket.SOL_SOCKET,
|
||||
|
||||
@@ -4,7 +4,7 @@ from trytond.protocols.sslsocket import SSLSocket
|
||||
from trytond.protocols.dispatcher import dispatch
|
||||
from trytond.config import CONFIG
|
||||
from trytond.protocols.datatype import Float
|
||||
from trytond.protocols.common import daemon
|
||||
from trytond.protocols.common import daemon, GZipRequestHandlerMixin
|
||||
from trytond import security
|
||||
import SimpleXMLRPCServer
|
||||
import SocketServer
|
||||
@@ -13,8 +13,6 @@ import traceback
|
||||
import socket
|
||||
import sys
|
||||
import os
|
||||
import gzip
|
||||
import StringIO
|
||||
import base64
|
||||
import datetime
|
||||
from types import DictType
|
||||
@@ -102,8 +100,10 @@ class GenericXMLRPCRequestHandler:
|
||||
security.logout(database_name, user, session)
|
||||
|
||||
|
||||
class SimpleXMLRPCRequestHandler(GenericXMLRPCRequestHandler,
|
||||
class SimpleXMLRPCRequestHandler(GZipRequestHandlerMixin,
|
||||
GenericXMLRPCRequestHandler,
|
||||
SimpleXMLRPCServer.SimpleXMLRPCRequestHandler):
|
||||
protocol_version = "HTTP/1.1"
|
||||
rpc_paths = None
|
||||
encode_threshold = 1400 # common MTU
|
||||
|
||||
@@ -127,69 +127,6 @@ class SimpleXMLRPCRequestHandler(GenericXMLRPCRequestHandler,
|
||||
self.send_header("WWW-Authenticate", 'Basic realm="Tryton"')
|
||||
return False
|
||||
|
||||
# Copy from SimpleXMLRPCServer.py with gzip encoding added
|
||||
def do_POST(self):
|
||||
"""Handles the HTTP POST request.
|
||||
|
||||
Attempts to interpret all HTTP POST requests as XML-RPC calls,
|
||||
which are forwarded to the server's _dispatch method for handling.
|
||||
"""
|
||||
|
||||
# Check that the path is legal
|
||||
if not self.is_rpc_path_valid():
|
||||
self.report_404()
|
||||
return
|
||||
|
||||
try:
|
||||
# Get arguments by reading body of request.
|
||||
# We read this in chunks to avoid straining
|
||||
# socket.read(); around the 10 or 15Mb mark, some platforms
|
||||
# begin to have problems (bug #792570).
|
||||
max_chunk_size = 10*1024*1024
|
||||
size_remaining = int(self.headers["content-length"])
|
||||
L = []
|
||||
while size_remaining:
|
||||
chunk_size = min(size_remaining, max_chunk_size)
|
||||
L.append(self.rfile.read(chunk_size))
|
||||
size_remaining -= len(L[-1])
|
||||
data = ''.join(L)
|
||||
|
||||
# In previous versions of SimpleXMLRPCServer, _dispatch
|
||||
# could be overridden in this class, instead of in
|
||||
# SimpleXMLRPCDispatcher. To maintain backwards compatibility,
|
||||
# check to see if a subclass implements _dispatch and dispatch
|
||||
# using that method if present.
|
||||
response = self.server._marshaled_dispatch(
|
||||
data, getattr(self, '_dispatch', None)
|
||||
)
|
||||
except Exception: # This should only happen if the module is buggy
|
||||
# internal error, report as HTTP server error
|
||||
self.send_response(500)
|
||||
self.end_headers()
|
||||
else:
|
||||
# got a valid XML RPC response
|
||||
self.send_response(200)
|
||||
self.send_header("Content-type", "text/xml")
|
||||
|
||||
# Handle gzip encoding
|
||||
if 'gzip' in self.headers.get('Accept-Encoding', '').split(',') \
|
||||
and len(response) > self.encode_threshold:
|
||||
buffer = StringIO.StringIO()
|
||||
output = gzip.GzipFile(mode='wb', fileobj=buffer)
|
||||
output.write(response)
|
||||
output.close()
|
||||
buffer.seek(0)
|
||||
response = buffer.getvalue()
|
||||
self.send_header('Content-Encoding', 'gzip')
|
||||
|
||||
self.send_header("Content-length", str(len(response)))
|
||||
self.end_headers()
|
||||
self.wfile.write(response)
|
||||
|
||||
# shut down the connection
|
||||
self.wfile.flush()
|
||||
self.connection.shutdown(1)
|
||||
|
||||
|
||||
class SecureXMLRPCRequestHandler(SimpleXMLRPCRequestHandler):
|
||||
|
||||
@@ -202,6 +139,7 @@ class SecureXMLRPCRequestHandler(SimpleXMLRPCRequestHandler):
|
||||
class SimpleThreadedXMLRPCServer(SocketServer.ThreadingMixIn,
|
||||
SimpleXMLRPCServer.SimpleXMLRPCServer):
|
||||
timeout = 1
|
||||
daemon_threads = True
|
||||
|
||||
def server_bind(self):
|
||||
self.socket.setsockopt(socket.SOL_SOCKET,
|
||||
|
||||
@@ -1,253 +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 socket
|
||||
# can't use/fall-back pickle due to different interface :-(
|
||||
import cPickle
|
||||
try:
|
||||
import cStringIO as StringIO
|
||||
except ImportError:
|
||||
import StringIO
|
||||
try:
|
||||
import ssl
|
||||
except ImportError:
|
||||
ssl = None
|
||||
import gzip
|
||||
try:
|
||||
import hashlib
|
||||
except ImportError:
|
||||
hashlib = None
|
||||
import sha
|
||||
import os
|
||||
|
||||
DNS_CACHE = {}
|
||||
MAX_SIZE = 999999999
|
||||
MAX_LENGHT = len(str(MAX_SIZE))
|
||||
CONNECT_TIMEOUT = 5
|
||||
TIMEOUT = 3600
|
||||
GZIP_THRESHOLD = 1400 # common MTU
|
||||
|
||||
_ALLOWED_MODULES = {'datetime': ['datetime', 'date'], 'decimal': ['Decimal']}
|
||||
|
||||
def checkfunction(module, klass):
|
||||
if module in _ALLOWED_MODULES and klass in _ALLOWED_MODULES[module]:
|
||||
mod = __import__(module, {}, {}, ['__all__'])
|
||||
_class = getattr(mod, klass)
|
||||
return _class
|
||||
raise ValueError('Not supported: %s/%s' % (module, klass))
|
||||
|
||||
|
||||
class PySocket:
|
||||
|
||||
def __init__(self, sock=None, fingerprints=None, ca_certs=None):
|
||||
self.sock = sock
|
||||
self.host = None
|
||||
self.hostname = None
|
||||
self.port = None
|
||||
self.ssl = False
|
||||
self.ssl_sock = None
|
||||
self.connected = False
|
||||
self.buffer = ''
|
||||
self.fingerprints = fingerprints
|
||||
if ca_certs and os.path.isfile(ca_certs):
|
||||
self.ca_certs = ca_certs
|
||||
else:
|
||||
self.ca_certs = None
|
||||
|
||||
def connect(self, host, port=False):
|
||||
if not port:
|
||||
buf = host.split('//')[1]
|
||||
host, port = buf.rsplit(':', 1)
|
||||
hostname = host
|
||||
if host in DNS_CACHE:
|
||||
host = DNS_CACHE[host]
|
||||
self.sock = None
|
||||
if socket.has_ipv6:
|
||||
try:
|
||||
socket.getaddrinfo(host, int(port), socket.AF_INET6)
|
||||
self.sock = socket.socket(socket.AF_INET6,
|
||||
socket.SOCK_STREAM)
|
||||
self.sock.settimeout(CONNECT_TIMEOUT)
|
||||
self.sock.connect((host, int(port)))
|
||||
except socket.error:
|
||||
self.sock = None
|
||||
if self.sock is None:
|
||||
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.sock.settimeout(CONNECT_TIMEOUT)
|
||||
self.sock.connect((host, int(port)))
|
||||
DNS_CACHE[hostname], port = self.sock.getpeername()[:2]
|
||||
try:
|
||||
sock = None
|
||||
if socket.has_ipv6:
|
||||
try:
|
||||
socket.getaddrinfo(host, int(port), socket.AF_INET6)
|
||||
sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
|
||||
sock.settimeout(CONNECT_TIMEOUT)
|
||||
sock.connect((host, int(port)))
|
||||
except socket.error:
|
||||
sock = None
|
||||
if sock is None:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(CONNECT_TIMEOUT)
|
||||
sock.connect((host, int(port)))
|
||||
if ssl:
|
||||
ssl_sock = ssl.wrap_socket(sock)
|
||||
self.ssl = True
|
||||
elif hasattr(socket, 'ssl'):
|
||||
ssl_sock = socket.ssl(sock)
|
||||
self.ssl = True
|
||||
except socket.error:
|
||||
pass
|
||||
self.sock.settimeout(TIMEOUT)
|
||||
fingerprint = None
|
||||
if self.ssl:
|
||||
if ssl:
|
||||
self.ssl_sock = ssl.wrap_socket(self.sock,
|
||||
ca_certs=self.ca_certs, cert_reqs=(self.ca_certs and
|
||||
ssl.CERT_REQUIRED or ssl.CERT_NONE))
|
||||
elif hasattr(socket, 'ssl'):
|
||||
self.ssl_sock = socket.ssl(self.sock, certfile=self.ca_certs)
|
||||
try:
|
||||
peercert = self.ssl_sock.getpeercert(True)
|
||||
except socket.error:
|
||||
peercert = None
|
||||
def format_hash(value):
|
||||
return reduce(lambda x, y: x + y[1].upper() +
|
||||
((y[0] % 2 and y[0] + 1 < len(value)) and ':' or ''),
|
||||
enumerate(value), '')
|
||||
if peercert and hashlib:
|
||||
fingerprint = format_hash(hashlib.sha1(peercert).hexdigest())
|
||||
elif peercert:
|
||||
fingerprint = format_hash(sha1.new(peercert).hexdigest())
|
||||
self.host = host
|
||||
self.hostname = hostname
|
||||
self.port = port
|
||||
self.connected = True
|
||||
self.buffer = ''
|
||||
if self.fingerprints is not None:
|
||||
key = (self.hostname, str(self.port))
|
||||
if key in self.fingerprints:
|
||||
if self.fingerprints[key] != fingerprint:
|
||||
self.disconnect()
|
||||
raise Exception('BadFingerprint')
|
||||
elif fingerprint:
|
||||
self.fingerprints[key] = fingerprint
|
||||
|
||||
def disconnect(self):
|
||||
try:
|
||||
sock = self.sock
|
||||
if self.ssl:
|
||||
sock = self.ssl_sock
|
||||
try:
|
||||
shutdown_value = 2
|
||||
if hasattr(socket, 'SHUT_RDWR'):
|
||||
shutdown_value = socket.SHUT_RDWR
|
||||
if hasattr(sock, 'sock_shutdown'):
|
||||
sock.sock_shutdown(shutdown_value)
|
||||
else:
|
||||
sock.shutdown(shutdown_value)
|
||||
except socket.error:
|
||||
pass
|
||||
sock.close()
|
||||
except socket.error:
|
||||
pass
|
||||
self.sock = None
|
||||
self.ssl = False
|
||||
self.ssl_sock = None
|
||||
self.connected = False
|
||||
self.buffer = ''
|
||||
|
||||
def reconnect(self):
|
||||
if self.hostname and self.port:
|
||||
self.disconnect()
|
||||
self.connect(self.hostname, self.port)
|
||||
|
||||
def send(self, msg, exception=False, traceback=None):
|
||||
msg = cPickle.dumps([msg, traceback], protocol=2)
|
||||
gzip_p = False
|
||||
if len(msg) > GZIP_THRESHOLD:
|
||||
buffer = StringIO.StringIO()
|
||||
output = gzip.GzipFile(mode='wb', fileobj=buffer)
|
||||
output.write(msg)
|
||||
output.close()
|
||||
buffer.seek(0)
|
||||
msg = buffer.getvalue()
|
||||
gzip_p = True
|
||||
size = len(msg)
|
||||
msg = str(size) + ' ' + (exception and "1" or "0") \
|
||||
+ (gzip_p and "1" or "0") + msg
|
||||
size = len(msg)
|
||||
|
||||
if self.ssl:
|
||||
totalsent = 0
|
||||
while totalsent < size:
|
||||
sent = self.ssl_sock.write(msg[totalsent:])
|
||||
if sent == 0:
|
||||
raise RuntimeError("socket connection broken")
|
||||
totalsent = totalsent + sent
|
||||
else:
|
||||
self.sock.sendall(msg)
|
||||
|
||||
def receive(self):
|
||||
buf = self.buffer
|
||||
L = []
|
||||
size_remaining = MAX_LENGHT
|
||||
while size_remaining:
|
||||
chunk_size = min(size_remaining, 4096)
|
||||
if self.ssl:
|
||||
chunk = self.ssl_sock.read(chunk_size)
|
||||
else:
|
||||
chunk = self.sock.recv(chunk_size)
|
||||
if chunk == '':
|
||||
raise RuntimeError("socket connection broken")
|
||||
L.append(chunk)
|
||||
size_remaining -= len(chunk)
|
||||
if ' ' in chunk:
|
||||
break
|
||||
if size_remaining < 0:
|
||||
raise RuntimeError("socket connection broken")
|
||||
buf += ''.join(L)
|
||||
size, msg = buf.split(' ', 1)
|
||||
size = int(size)
|
||||
if size > MAX_SIZE:
|
||||
raise RuntimeError("socket connection broken")
|
||||
while len(msg) < 2:
|
||||
chunk_size = min(size + 2, 4096)
|
||||
if self.ssl:
|
||||
msg += self.ssl_sock.read(chunk_size)
|
||||
else:
|
||||
msg += self.sock.recv(chunk_size)
|
||||
if msg == '':
|
||||
raise RuntimeError("socket connection broken")
|
||||
exception = msg[0] != "0"
|
||||
gzip_p = msg[1] != "0"
|
||||
L = [msg[2:]]
|
||||
size_remaining = size - len(L[0])
|
||||
while size_remaining:
|
||||
chunk_size = min(size_remaining, 4096)
|
||||
if self.ssl:
|
||||
chunk = self.ssl_sock.read(chunk_size)
|
||||
else:
|
||||
chunk = self.sock.recv(chunk_size)
|
||||
if chunk == '':
|
||||
raise RuntimeError("socket connection broken")
|
||||
L.append(chunk)
|
||||
size_remaining -= len(chunk)
|
||||
msg = ''.join(L)
|
||||
if len(msg) > size:
|
||||
self.buffer = msg[size:]
|
||||
msg = msg[:size]
|
||||
else:
|
||||
self.buffer = ''
|
||||
msgio = StringIO.StringIO(msg)
|
||||
if gzip_p:
|
||||
output = gzip.GzipFile(mode='r', fileobj=msgio)
|
||||
msgio = StringIO.StringIO(output.read(-1))
|
||||
output.close()
|
||||
unpickler = cPickle.Unpickler(msgio)
|
||||
# cPickle mechanism to import instances (pickle differs here)
|
||||
unpickler.find_global = checkfunction
|
||||
res = unpickler.load()
|
||||
if exception:
|
||||
raise Exception(*(list(res[0]) + [res[1]]))
|
||||
else:
|
||||
return res[0]
|
||||
+54
-60
@@ -1,8 +1,5 @@
|
||||
#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
|
||||
import xml
|
||||
import sys
|
||||
import base64
|
||||
try:
|
||||
import cStringIO as StringIO
|
||||
@@ -13,11 +10,10 @@ import time
|
||||
import os
|
||||
import datetime
|
||||
from base64 import decodestring
|
||||
import traceback
|
||||
import inspect
|
||||
import logging
|
||||
import tempfile
|
||||
import warnings
|
||||
import subprocess
|
||||
warnings.simplefilter("ignore")
|
||||
import relatorio.reporting
|
||||
warnings.resetwarnings()
|
||||
@@ -28,11 +24,36 @@ except ImportError:
|
||||
from genshi.filters import Translator
|
||||
import lxml.etree
|
||||
from trytond.config import CONFIG
|
||||
from trytond.backend import DatabaseIntegrityError
|
||||
from trytond.pool import Pool
|
||||
from trytond.transaction import Transaction
|
||||
from trytond.url import URLMixin
|
||||
|
||||
MIMETYPES = {
|
||||
'odt': 'application/vnd.oasis.opendocument.text',
|
||||
'odp': 'application/vnd.oasis.opendocument.presentation',
|
||||
'ods': 'application/vnd.oasis.opendocument.spreadsheet',
|
||||
'odg': 'application/vnd.oasis.opendocument.graphics',
|
||||
}
|
||||
FORMAT2EXT = {
|
||||
'doc6': 'doc',
|
||||
'doc95': 'doc',
|
||||
'docbook': 'xml',
|
||||
'ooxml': 'xml',
|
||||
'latex': 'ltx',
|
||||
'sdc4': 'sdc',
|
||||
'sdc3': 'sdc',
|
||||
'sdd3': 'sdd',
|
||||
'sdd4': 'sdd',
|
||||
'sdw4': 'sdw',
|
||||
'sdw3': 'sdw',
|
||||
'sxd3': 'sxd',
|
||||
'sxd5': 'sxd',
|
||||
'text': 'txt',
|
||||
'xhtml': 'html',
|
||||
'xls5': 'xls',
|
||||
'xls95': 'xls',
|
||||
}
|
||||
|
||||
|
||||
class ReportFactory:
|
||||
|
||||
@@ -150,7 +171,9 @@ class Report(URLMixin):
|
||||
if not report.report_content:
|
||||
raise Exception('Error', 'Missing report file!')
|
||||
|
||||
fd, path = tempfile.mkstemp(suffix='.odt', prefix='trytond')
|
||||
fd, path = tempfile.mkstemp(
|
||||
suffix=(os.extsep + report.template_extension),
|
||||
prefix='trytond_')
|
||||
outzip = zipfile.ZipFile(path, mode='w')
|
||||
|
||||
content_io = StringIO.StringIO()
|
||||
@@ -220,7 +243,8 @@ class Report(URLMixin):
|
||||
# Since Genshi >= 0.6, Translator requires a function type
|
||||
translator = Translator(lambda text: translate(text))
|
||||
|
||||
rel_report = relatorio.reporting.Report(path, 'application/vnd.oasis.opendocument.text',
|
||||
mimetype = MIMETYPES[report.template_extension]
|
||||
rel_report = relatorio.reporting.Report(path, mimetype,
|
||||
ReportFactory(), relatorio.reporting.MIMETemplateLoader())
|
||||
rel_report.filters.insert(0, translator)
|
||||
#convert unicode key into str
|
||||
@@ -236,61 +260,31 @@ class Report(URLMixin):
|
||||
data = data.getvalue()
|
||||
os.close(fd)
|
||||
os.remove(path)
|
||||
output_format = report.extension
|
||||
if output_format == 'pdf':
|
||||
data = self.convert_pdf(data)
|
||||
return (output_format, data)
|
||||
output_format = report.extension or report.template_extension
|
||||
if output_format not in MIMETYPES:
|
||||
data = self.unoconv(data, report.template_extension, output_format)
|
||||
oext = FORMAT2EXT.get(output_format, output_format)
|
||||
return (oext, data)
|
||||
|
||||
def convert_pdf(self, data):
|
||||
"""
|
||||
Convert report to PDF using OpenOffice.org.
|
||||
This requires OpenOffice.org, pyuno and openoffice-python to
|
||||
be installed.
|
||||
"""
|
||||
import tempfile
|
||||
def unoconv(self, data, input_format, output_format):
|
||||
'''
|
||||
Call unoconv to convert the OpenDocument
|
||||
'''
|
||||
fd, path = tempfile.mkstemp(suffix=(os.extsep + input_format),
|
||||
prefix='trytond_')
|
||||
oext = FORMAT2EXT.get(output_format, output_format)
|
||||
with os.fdopen(fd, 'wb+') as fp:
|
||||
fp.write(data)
|
||||
cmd = ['unoconv', '--connection=%s' % CONFIG['unoconv'],
|
||||
'-f', oext, '--stdout', path]
|
||||
try:
|
||||
import unohelper # installs import-hook
|
||||
import openoffice.interact
|
||||
import openoffice.officehelper as officehelper
|
||||
from openoffice.streams import OutputStream
|
||||
from com.sun.star.beans import PropertyValue
|
||||
except ImportError, exception:
|
||||
raise Exception('ImportError', str(exception))
|
||||
try:
|
||||
# connect to OOo
|
||||
desktop = openoffice.interact.Desktop()
|
||||
except officehelper.BootstrapException:
|
||||
raise Exception('Error', "Can't connect to (bootstrap) OpenOffice.org")
|
||||
|
||||
res_data = None
|
||||
# Create temporary file (with name) and write data there.
|
||||
# We can not use NamedTemporaryFile here, since this would be
|
||||
# deleted as soon as we close it to allow OOo reading.
|
||||
#TODO use an input stream here
|
||||
fd_odt, odt_name = tempfile.mkstemp()
|
||||
fh_odt = os.fdopen(fd_odt, 'wb+')
|
||||
try:
|
||||
fh_odt.write(data)
|
||||
del data # save memory
|
||||
fh_odt.close()
|
||||
doc = desktop.openFile(odt_name, hidden=False)
|
||||
# Export as PDF
|
||||
buffer = StringIO.StringIO()
|
||||
out_props = (
|
||||
PropertyValue("FilterName", 0, "writer_pdf_Export", 0),
|
||||
PropertyValue("Overwrite", 0, True, 0),
|
||||
PropertyValue("OutputStream", 0, OutputStream(buffer), 0),
|
||||
)
|
||||
doc.storeToURL("private:stream", out_props)
|
||||
res_data = buffer.getvalue()
|
||||
del buffer
|
||||
doc.dispose()
|
||||
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE)
|
||||
stdoutdata, stderrdata = proc.communicate()
|
||||
if proc.wait() != 0:
|
||||
raise Exception(stderrdata)
|
||||
return stdoutdata
|
||||
finally:
|
||||
fh_odt.close()
|
||||
os.remove(odt_name)
|
||||
if not res_data:
|
||||
Exception('Error', 'Error converting to PDF')
|
||||
return res_data
|
||||
os.remove(path)
|
||||
|
||||
def format_lang(self, value, lang, digits=2, grouping=True, monetary=False,
|
||||
date=False, currency=None, symbol=True):
|
||||
|
||||
+15
-14
@@ -25,38 +25,39 @@ _READONLY = If(In(Eval('state'), ['waiting', 'closed']),
|
||||
If(Equal(Eval('state'), 'chatting'),
|
||||
Not(Equal(Eval('act_from'), Eval('_user'))),
|
||||
False))
|
||||
_DEPENDS = ['state', 'act_from']
|
||||
|
||||
class Request(ModelSQL, ModelView):
|
||||
"Request"
|
||||
_name = 'res.request'
|
||||
_description = __doc__
|
||||
name = fields.Char('Subject', states={
|
||||
'readonly': _READONLY,
|
||||
}, required=True)
|
||||
'readonly': _READONLY,
|
||||
}, required=True, depends=_DEPENDS)
|
||||
active = fields.Boolean('Active')
|
||||
priority = fields.Selection(_PRIORITIES, 'Priority', states={
|
||||
'readonly': _READONLY,
|
||||
}, required=True, order_field='priority')
|
||||
'readonly': _READONLY,
|
||||
}, required=True, order_field='priority', depends=_DEPENDS)
|
||||
act_from = fields.Many2One('res.user', 'From', required=True,
|
||||
readonly=True)
|
||||
act_to = fields.Many2One('res.user', 'To', required=True,
|
||||
domain=[('active', '=', True)],
|
||||
states={
|
||||
'readonly': _READONLY,
|
||||
})
|
||||
}, depends=_DEPENDS)
|
||||
body = fields.Text('Body', states={
|
||||
'readonly': _READONLY,
|
||||
})
|
||||
'readonly': _READONLY,
|
||||
}, depends=_DEPENDS)
|
||||
date_sent = fields.DateTime('Date', readonly=True)
|
||||
trigger_date = fields.DateTime('Trigger Date', states={
|
||||
'readonly': _READONLY,
|
||||
})
|
||||
'readonly': _READONLY,
|
||||
}, depends=_DEPENDS)
|
||||
references = fields.One2Many('res.request.reference', 'request',
|
||||
'References', states={
|
||||
'readonly': If(Equal(Eval('state'), 'closed'),
|
||||
True,
|
||||
Not(Equal(Eval('act_from', 0), Eval('_user', 0)))),
|
||||
})
|
||||
'References', states={
|
||||
'readonly': If(Equal(Eval('state'), 'closed'),
|
||||
True,
|
||||
Not(Equal(Eval('act_from', 0), Eval('_user', 0)))),
|
||||
}, depends=['state', 'act_from'])
|
||||
number_references = fields.Function(fields.Integer('Number of References',
|
||||
on_change_with=['references']), 'get_number_references')
|
||||
state = fields.Selection(_STATES, 'State', required=True, readonly=True)
|
||||
|
||||
+3
-3
@@ -16,7 +16,7 @@ from trytond.backend import TableHandler
|
||||
from trytond.security import get_connections
|
||||
from trytond.transaction import Transaction
|
||||
from trytond.cache import Cache
|
||||
from trytond.pyson import Eval, Greater
|
||||
from trytond.pyson import Eval, Bool
|
||||
from trytond.pool import Pool
|
||||
|
||||
|
||||
@@ -31,8 +31,8 @@ class User(ModelSQL, ModelView):
|
||||
signature = fields.Text('Signature')
|
||||
active = fields.Boolean('Active')
|
||||
action = fields.Many2One('ir.action', 'Home Action', states={
|
||||
'required': Greater(Eval('active_id', 0), 0),
|
||||
})
|
||||
'required': Bool(Eval('id')),
|
||||
}, depends=['id'])
|
||||
menu = fields.Many2One('ir.action', 'Menu Action',
|
||||
domain=[('usage','=','menu')], required=True)
|
||||
groups = fields.Many2Many('res.user-res.group',
|
||||
|
||||
+30
-45
@@ -154,55 +154,45 @@ class TrytonServer(object):
|
||||
sys.exit(0)
|
||||
|
||||
# Launch Server
|
||||
if CONFIG['xmlrpc']:
|
||||
from trytond.protocols.xmlrpc import XMLRPCDaemon
|
||||
xmlrpcd = XMLRPCDaemon(CONFIG['interface'], CONFIG['xmlport'],
|
||||
CONFIG['secure_xmlrpc'])
|
||||
self.logger.info("starting XML-RPC%s protocol, port %d" % \
|
||||
(CONFIG['secure_xmlrpc'] and ' Secure' or '',
|
||||
CONFIG['xmlport']))
|
||||
|
||||
jsonrpcd = []
|
||||
if CONFIG['jsonrpc']:
|
||||
from trytond.protocols.jsonrpc import JSONRPCDaemon
|
||||
jsonrpcd = JSONRPCDaemon(CONFIG['interface'], CONFIG['jsonport'],
|
||||
CONFIG['secure_jsonrpc'])
|
||||
self.logger.info("starting JSON-RPC%s protocol, port %d" % \
|
||||
(CONFIG['secure_jsonrpc'] and ' Secure' or '',
|
||||
CONFIG['jsonport']))
|
||||
for hostname, port in CONFIG['jsonrpc']:
|
||||
jsonrpcd.append(JSONRPCDaemon(hostname, port,
|
||||
CONFIG['ssl_jsonrpc']))
|
||||
self.logger.info("starting JSON-RPC%s protocol on %s:%d" %
|
||||
(CONFIG['ssl_jsonrpc'] and ' SSL' or '', hostname or '*',
|
||||
port))
|
||||
|
||||
if CONFIG['netrpc']:
|
||||
from trytond.protocols.netrpc import NetRPCServerThread
|
||||
netrpcd = NetRPCServerThread(CONFIG['interface'], CONFIG['netport'],
|
||||
CONFIG['secure_netrpc'])
|
||||
self.logger.info("starting NetRPC%s protocol, port %d" % \
|
||||
(CONFIG['secure_netrpc'] and ' Secure' or '',
|
||||
CONFIG['netport']))
|
||||
xmlrpcd = []
|
||||
if CONFIG['xmlrpc']:
|
||||
from trytond.protocols.xmlrpc import XMLRPCDaemon
|
||||
for hostname, port in CONFIG['xmlrpc']:
|
||||
xmlrpcd.append(XMLRPCDaemon(hostname, port,
|
||||
CONFIG['ssl_xmlrpc']))
|
||||
self.logger.info("starting XML-RPC%s protocol on %s:%d" %
|
||||
(CONFIG['ssl_xmlrpc'] and ' SSL' or '', hostname or '*',
|
||||
port))
|
||||
|
||||
webdavd = []
|
||||
if CONFIG['webdav']:
|
||||
from trytond.protocols.webdav import WebDAVServerThread
|
||||
webdavd = WebDAVServerThread(CONFIG['interface'],
|
||||
CONFIG['webdavport'], CONFIG['secure_webdav'])
|
||||
self.logger.info("starting WebDAV%s protocol, port %d" % \
|
||||
(CONFIG['secure_webdav'] and ' Secure' or '',
|
||||
CONFIG['webdavport']))
|
||||
for hostname, port in CONFIG['webdav']:
|
||||
webdavd.append(WebDAVServerThread(hostname, port,
|
||||
CONFIG['ssl_webdav']))
|
||||
self.logger.info("starting WebDAV%s protocol on %s:%d" %
|
||||
(CONFIG['ssl_webdav'] and ' SSL' or '', hostname or '*',
|
||||
port))
|
||||
|
||||
def handler(signum, frame):
|
||||
if hasattr(signal, 'SIGUSR1'):
|
||||
if signum == signal.SIGUSR1:
|
||||
Pool.start()
|
||||
return
|
||||
if CONFIG['netrpc']:
|
||||
netrpcd.stop()
|
||||
netrpcd.join()
|
||||
if CONFIG['xmlrpc']:
|
||||
xmlrpcd.stop()
|
||||
xmlrpcd.join()
|
||||
if CONFIG['jsonrpc']:
|
||||
jsonrpcd.stop()
|
||||
jsonrpcd.join()
|
||||
if CONFIG['webdav']:
|
||||
webdavd.stop()
|
||||
webdavd.join()
|
||||
for servers in (xmlrpcd, jsonrpcd, webdavd):
|
||||
for server in servers:
|
||||
server.stop()
|
||||
server.join()
|
||||
if CONFIG['pidfile']:
|
||||
os.unlink(CONFIG['pidfile'])
|
||||
logging.getLogger('server').info('stopped')
|
||||
@@ -221,14 +211,9 @@ class TrytonServer(object):
|
||||
signal.signal(signal.SIGUSR1, handler)
|
||||
|
||||
self.logger.info('waiting for connections...')
|
||||
if CONFIG['netrpc']:
|
||||
netrpcd.start()
|
||||
if CONFIG['xmlrpc']:
|
||||
xmlrpcd.start()
|
||||
if CONFIG['jsonrpc']:
|
||||
jsonrpcd.start()
|
||||
if CONFIG['webdav']:
|
||||
webdavd.start()
|
||||
for servers in (xmlrpcd, jsonrpcd, webdavd):
|
||||
for server in servers:
|
||||
server.start()
|
||||
|
||||
if CONFIG['psyco']:
|
||||
import psyco
|
||||
|
||||
@@ -100,7 +100,8 @@ class FloatDigits(ModelSQL):
|
||||
_description = __doc__
|
||||
digits = fields.Integer('Digits')
|
||||
float = fields.Float(string='Float', help='Test float',
|
||||
required=False, digits=(16, Eval('digits', 2)))
|
||||
required=False, digits=(16, Eval('digits', 2)),
|
||||
depends=['digits'])
|
||||
|
||||
FloatDigits()
|
||||
|
||||
@@ -144,7 +145,8 @@ class NumericDigits(ModelSQL):
|
||||
_description = __doc__
|
||||
digits = fields.Integer('Digits')
|
||||
numeric = fields.Numeric(string='Numeric', help='Test numeric',
|
||||
required=False, digits=(16, Eval('digits', 2)))
|
||||
required=False, digits=(16, Eval('digits', 2)),
|
||||
depends=['digits'])
|
||||
|
||||
NumericDigits()
|
||||
|
||||
|
||||
@@ -26,28 +26,28 @@ class UrlTestCase(unittest.TestCase):
|
||||
"Test model URLs"
|
||||
with Transaction().start(DB_NAME, USER, CONTEXT):
|
||||
self.assertEqual(self.urlmodel.get_url(),
|
||||
'tryton://%s:%s/%s/model/test.urlobject' % (self.hostname,
|
||||
CONFIG['netport'], urllib.quote(DB_NAME)))
|
||||
'tryton://%s/%s/model/test.urlobject' % (self.hostname,
|
||||
urllib.quote(DB_NAME)))
|
||||
|
||||
server_name = 'michaelscott.paper.test'
|
||||
CONFIG['hostname'] = server_name
|
||||
self.assertEqual(self.urlmodel.get_url(),
|
||||
'tryton://%s:%s/%s/model/test.urlobject' % (server_name,
|
||||
CONFIG['netport'], urllib.quote(DB_NAME)))
|
||||
'tryton://%s/%s/model/test.urlobject' % (server_name,
|
||||
urllib.quote(DB_NAME)))
|
||||
|
||||
def testWizardURL(self):
|
||||
"Test wizard URLs"
|
||||
with Transaction().start(DB_NAME, USER, CONTEXT):
|
||||
CONFIG['hostname'] = None
|
||||
self.assertEqual(self.urlwizard.get_url(),
|
||||
'tryton://%s:%s/%s/wizard/test.test_wizard' % (self.hostname,
|
||||
CONFIG['netport'], urllib.quote(DB_NAME)))
|
||||
'tryton://%s/%s/wizard/test.test_wizard' % (self.hostname,
|
||||
urllib.quote(DB_NAME)))
|
||||
|
||||
server_name = 'michaelscott.paper.test'
|
||||
CONFIG['hostname'] = server_name
|
||||
self.assertEqual(self.urlwizard.get_url(),
|
||||
'tryton://%s:%s/%s/wizard/test.test_wizard' % (server_name,
|
||||
CONFIG['netport'], urllib.quote(DB_NAME)))
|
||||
'tryton://%s/%s/wizard/test.test_wizard' % (server_name,
|
||||
urllib.quote(DB_NAME)))
|
||||
|
||||
|
||||
def suite():
|
||||
|
||||
@@ -33,6 +33,7 @@ from trytond.pool import Pool
|
||||
from trytond.backend import Database
|
||||
from trytond.protocols.dispatcher import create
|
||||
from trytond.transaction import Transaction
|
||||
from trytond.pyson import PYSONEncoder, Eval
|
||||
|
||||
register_classes()
|
||||
|
||||
@@ -87,6 +88,22 @@ class ModelViewTestCase(unittest.TestCase):
|
||||
'''
|
||||
test_view('webdav')
|
||||
|
||||
|
||||
class FieldDependsTestCase(unittest.TestCase):
|
||||
'''
|
||||
Test Field depends
|
||||
'''
|
||||
|
||||
def setUp(self):
|
||||
install_module('ir')
|
||||
install_module('res')
|
||||
install_module('workflow')
|
||||
install_module('webdav')
|
||||
|
||||
def test0010depends(self):
|
||||
test_depends()
|
||||
|
||||
|
||||
def install_module(name):
|
||||
'''
|
||||
Install module for the tested database
|
||||
@@ -154,6 +171,39 @@ def test_view(module_name):
|
||||
% field)
|
||||
transaction.cursor.rollback()
|
||||
|
||||
def test_depends():
|
||||
'''
|
||||
Test for missing depends
|
||||
'''
|
||||
class Encoder(PYSONEncoder):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Encoder, self).__init__(*args, **kwargs)
|
||||
self.fields = set()
|
||||
|
||||
def default(self, obj):
|
||||
if isinstance(obj, Eval):
|
||||
fname = obj._value
|
||||
if not fname.startswith('_parent_'):
|
||||
self.fields.add(fname)
|
||||
return super(Encoder, self).default(obj)
|
||||
|
||||
with Transaction().start(DB_NAME, USER, CONTEXT) as transaction:
|
||||
for mname, model in Pool().iterobject():
|
||||
for fname, field in model._columns.iteritems():
|
||||
encoder = Encoder()
|
||||
for pyson in (field.domain, field.states):
|
||||
encoder.encode(pyson)
|
||||
if hasattr(field, 'digits'):
|
||||
encoder.encode(field.digits)
|
||||
encoder.fields.discard(fname)
|
||||
encoder.fields.discard('context')
|
||||
encoder.fields.discard('_user')
|
||||
depends = set(field.depends)
|
||||
assert encoder.fields <= depends, (
|
||||
'Missing depends %s in "%s"."%s"' % (
|
||||
list(encoder.fields - depends), mname, fname))
|
||||
|
||||
def suite():
|
||||
'''
|
||||
Return test suite for other modules
|
||||
@@ -175,6 +225,8 @@ def all_suite():
|
||||
suite_.addTests(test_modelsingleton.suite())
|
||||
suite_.addTests(unittest.TestLoader(
|
||||
).loadTestsFromTestCase(ModelViewTestCase))
|
||||
suite_.addTests(unittest.TestLoader(
|
||||
).loadTestsFromTestCase(FieldDependsTestCase))
|
||||
import trytond.tests.test_mptt as test_mptt
|
||||
suite_.addTests(test_mptt.suite())
|
||||
import trytond.tests.test_importdata as test_importdata
|
||||
|
||||
+3
-5
@@ -15,13 +15,12 @@ class URLMixin(object):
|
||||
from trytond.model import Model
|
||||
from trytond.wizard import Wizard
|
||||
from trytond.report import Report
|
||||
url_part = {}
|
||||
|
||||
hostname = CONFIG['hostname'] or unicode(socket.getfqdn(), 'utf8')
|
||||
url_part['hostname'] = '.'.join(encodings.idna.ToASCII(part) for part in
|
||||
hostname = '.'.join(encodings.idna.ToASCII(part) for part in
|
||||
hostname.split('.'))
|
||||
url_part['port'] = CONFIG['netport']
|
||||
|
||||
url_part = {}
|
||||
if isinstance(self, Model):
|
||||
url_part['type'] = 'model'
|
||||
elif isinstance(self, Wizard):
|
||||
@@ -34,6 +33,5 @@ class URLMixin(object):
|
||||
url_part['name'] = self._name
|
||||
url_part['database'] = Transaction().cursor.database_name
|
||||
|
||||
host_part = '%(hostname)s:%(port)s' % url_part
|
||||
local_part = urllib.quote('%(database)s/%(type)s/%(name)s' % url_part)
|
||||
return 'tryton://%s/%s' % (host_part, local_part)
|
||||
return 'tryton://%s/%s' % (hostname, local_part)
|
||||
|
||||
@@ -6,7 +6,7 @@ from trytond.model import ModelView, ModelSQL, fields
|
||||
from trytond.report import Report
|
||||
from trytond.tools import exec_command_pipe
|
||||
from trytond.backend import TableHandler
|
||||
from trytond.pyson import Eval, Equal, Not, Bool
|
||||
from trytond.pyson import Eval, Not, Bool
|
||||
from trytond.transaction import Transaction
|
||||
from trytond.pool import Pool
|
||||
|
||||
|
||||
Reference in New Issue
Block a user