This commit is contained in:
Nicolas Évrard
2011-08-11 16:13:27 +02:00
28 changed files with 587 additions and 762 deletions
+5
View File
@@ -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
+1
View File
@@ -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
+1 -1
View File
@@ -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
+3
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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 ''
+3 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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>
+2 -2
View File
@@ -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
View File
@@ -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()
+18 -71
View File
@@ -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
-139
View File
@@ -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
-1
View File
@@ -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,
+5 -67
View File
@@ -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,
-253
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+4 -2
View File
@@ -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()
+8 -8
View File
@@ -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():
+52
View File
@@ -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
View File
@@ -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)
+1 -1
View File
@@ -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