Files
tryton/tryton/common/common.py
T
Cédric Krier 4e0edda549 Replace Window.get_window_type by the property
Window.get_window_type is only available since PyGTK 2.22

issue2743
review501008
2012-09-10 13:24:02 +02:00

1495 lines
48 KiB
Python

#This file is part of Tryton. The COPYRIGHT file at the top level of
#this repository contains the full copyright notices and license terms.
import gtk
import gobject
import glib
import pango
import gettext
import os
import subprocess
import re
import logging
from tryton.config import CONFIG
from tryton.config import TRYTON_ICON, PIXMAPS_DIR
import time
import sys
import xmlrpclib
try:
import hashlib
except ImportError:
hashlib = None
import md5
import webbrowser
import traceback
import threading
import tryton.rpc as rpc
import locale
import socket
import tempfile
from tryton.version import VERSION
import thread
import urllib
from string import Template
import shlex
try:
import ssl
except ImportError:
ssl = None
import dis
from threading import Lock
try:
import pytz
except ImportError:
pytz = None
from tryton.exceptions import (TrytonServerError, TrytonError,
TrytonServerUnavailable)
_ = gettext.gettext
class TrytonIconFactory(gtk.IconFactory):
batchnum = 10
_tryton_icons = []
_name2id = {}
_locale_icons = set()
_loaded_icons = set()
def load_client_icons(self):
for fname in os.listdir(PIXMAPS_DIR):
name = os.path.splitext(fname)[0]
if not name.startswith('tryton-'):
continue
if not os.path.isfile(os.path.join(PIXMAPS_DIR, fname)):
continue
try:
pixbuf = gtk.gdk.pixbuf_new_from_file(
os.path.join(PIXMAPS_DIR, fname).decode('utf-8'))
except (IOError, glib.GError):
continue
finally:
self._locale_icons.add(name)
icon_set = gtk.IconSet(pixbuf)
self.add(name, icon_set)
for name in ('ok', 'cancel'):
icon_set = gtk.Style().lookup_icon_set('gtk-%s' % name)
self.add('tryton-%s' % name, icon_set)
self._locale_icons.add('tryton-%s' % name)
def load_icons(self, refresh=False):
if not refresh:
self._name2id.clear()
self._loaded_icons.clear()
del self._tryton_icons[:]
try:
icons = rpc.execute('model', 'ir.ui.icon', 'list_icons',
rpc.CONTEXT)
except TrytonServerError:
icons = []
for icon_id, icon_name in icons:
if refresh and icon_name in self._loaded_icons:
continue
self._tryton_icons.append((icon_id, icon_name))
self._name2id[icon_name] = icon_id
def register_icon(self, iconname):
# iconname might be '' when page do not define icon
if (not iconname
or iconname in (self._loaded_icons | self._locale_icons)):
return
if iconname not in self._name2id:
self.load_icons(refresh=True)
icon_ref = (self._name2id[iconname], iconname)
idx = self._tryton_icons.index(icon_ref)
to_load = slice(max(0, idx - self.batchnum // 2),
idx + self.batchnum // 2)
ids = [e[0] for e in self._tryton_icons[to_load]]
try:
icons = rpc.execute('model', 'ir.ui.icon', 'read', ids,
['name', 'icon'], rpc.CONTEXT)
except TrytonServerError:
icons = []
for icon in icons:
# svg file cannot be loaded from data into a pixbuf
fileno, path = tempfile.mkstemp()
with os.fdopen(fileno, 'w') as svgfile:
svgfile.write(icon['icon'])
try:
pixbuf = gtk.gdk.pixbuf_new_from_file(path.decode(
sys.getfilesystemencoding().encode('utf-8')))
except glib.GError:
continue
finally:
os.remove(path)
self._tryton_icons.remove((icon['id'], icon['name']))
del self._name2id[icon['name']]
self._loaded_icons.add(icon['name'])
iconset = gtk.IconSet(pixbuf)
self.add(icon['name'], iconset)
ICONFACTORY = TrytonIconFactory()
ICONFACTORY.add_default()
class ModelAccess(object):
batchnum = 100
_access = {}
_models = []
def load_models(self, refresh=False):
if not refresh:
self._access.clear()
del self._models[:]
try:
self._models = rpc.execute('model', 'ir.model', 'list_models',
rpc.CONTEXT)
except TrytonServerError:
pass
def __getitem__(self, model):
if model in self._access:
return self._access[model]
if model not in self._models:
self.load_models(refresh=True)
idx = self._models.index(model)
to_load = slice(max(0, idx - self.batchnum // 2),
idx + self.batchnum // 2)
try:
access = rpc.execute('model', 'ir.model.access', 'get_access',
self._models[to_load], rpc.CONTEXT)
except TrytonServerError:
access = {}
self._access.update(access)
return self._access[model]
MODELACCESS = ModelAccess()
def find_in_path(name):
if os.name == "nt":
sep = ';'
else:
sep = ':'
path = [directory for directory in os.environ['PATH'].split(sep)
if os.path.isdir(directory)]
for directory in path:
val = os.path.join(directory, name)
if os.path.isfile(val) or os.path.islink(val):
return val
return name
def test_server_version(host, port):
version = rpc.server_version(host, port)
if not version:
return False
return version.split('.')[:2] == VERSION.split('.')[:2]
def refresh_dblist(host, port):
'''
Return the number of database available
or None if it is impossible to connect
or -1 if the server version doesn't match the client version
'''
rpc.logout()
if not test_server_version(host, port):
return -1
return rpc.db_list(host, port)
def refresh_langlist(lang_widget, host, port):
liststore = lang_widget.get_model()
liststore.clear()
try:
lang_list = rpc.db_exec(host, port, 'list_lang')
except socket.error:
return []
from tryton.gui.main import Main
Main.get_main().refresh_ssl()
index = -1
i = 0
lang = locale.getdefaultlocale()[0]
for key, val in lang_list:
liststore.insert(i, (val, key))
if key == lang:
index = i
if key == 'en_US' and index < 0:
index = i
i += 1
lang_widget.set_active(index)
return lang_list
def request_server(server_widget):
result = False
parent = get_toplevel_window()
dialog = gtk.Dialog(
title=_('Tryton Connection'),
parent=parent,
flags=gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT |
gtk.WIN_POS_CENTER_ON_PARENT |
gtk.gdk.WINDOW_TYPE_HINT_DIALOG,)
dialog.set_has_separator(True)
vbox = gtk.VBox()
table = gtk.Table(2, 2, False)
table.set_border_width(12)
table.set_row_spacings(6)
vbox.pack_start(table, False, True, 0)
label_server = gtk.Label(_("Server:"))
label_server.set_alignment(1, 0)
label_server.set_padding(3, 0)
table.attach(label_server, 0, 1, 0, 1, yoptions=False,
xoptions=gtk.FILL)
entry_port = gtk.Entry()
entry_port.set_max_length(5)
entry_port.set_text("8000")
entry_port.set_activates_default(True)
entry_port.set_width_chars(16)
table.attach(entry_port, 1, 2, 1, 2, yoptions=False,
xoptions=gtk.FILL)
entry_server = gtk.Entry()
entry_server.set_text("localhost")
entry_server.set_activates_default(True)
entry_server.set_width_chars(16)
table.attach(entry_server, 1, 2, 0, 1, yoptions=False,
xoptions=gtk.FILL | gtk.EXPAND)
label_port = gtk.Label(_("Port:"))
label_port.set_alignment(1, 0.5)
label_port.set_padding(3, 3)
table.attach(label_port, 0, 1, 1, 2, yoptions=False,
xoptions=False)
dialog.add_button("gtk-cancel", gtk.RESPONSE_CANCEL | gtk.CAN_DEFAULT)
dialog.add_button("gtk-ok", gtk.RESPONSE_OK)
dialog.vbox.pack_start(vbox)
dialog.set_icon(TRYTON_ICON)
dialog.show_all()
dialog.set_default_response(gtk.RESPONSE_OK)
url_m = re.match('^([\w.-]+):(\d{1,5})',
server_widget.get_text())
if url_m:
entry_server.set_text(url_m.group(1))
entry_port.set_text(url_m.group(2))
res = dialog.run()
if res == gtk.RESPONSE_OK:
host = entry_server.get_text()
port = int(entry_port.get_text())
url = '%s:%d' % (host, port)
server_widget.set_text(url)
result = (host, port)
parent.present()
dialog.destroy()
return result
def get_toplevel_window():
windows = [x for x in gtk.window_list_toplevels()
if x.window and x.props.visible
and x.props.type == gtk.WINDOW_TOPLEVEL]
trans2windows = dict((x.get_transient_for(), x) for x in windows)
for window in set(windows) - set(trans2windows.iterkeys()):
return window
return trans2windows[None]
def get_sensible_widget(window):
from tryton.gui.main import Main
main = Main.get_main()
if main and window == main.window:
focus_widget = window.get_focus()
page = main.get_page()
if page and focus_widget and focus_widget.is_ancestor(page.widget):
return page.widget
return window
def center_window(window, parent, sensible):
parent_x, parent_y = parent.window.get_origin()
window_allocation = window.get_allocation()
sensible_allocation = sensible.get_allocation()
x = (parent_x + sensible_allocation.x +
int((sensible_allocation.width - window_allocation.width) / 2))
y = (parent_y + sensible_allocation.y +
int((sensible_allocation.height - window_allocation.height) / 2))
window.move(x, y)
def selection(title, values, alwaysask=False):
if not values or len(values) == 0:
return None
elif len(values) == 1 and (not alwaysask):
key = values.keys()[0]
return (key, values[key])
parent = get_toplevel_window()
dialog = gtk.Dialog(_('Selection'), parent,
gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
gtk.STOCK_OK, gtk.RESPONSE_OK))
dialog.set_icon(TRYTON_ICON)
dialog.set_has_separator(True)
dialog.set_default_response(gtk.RESPONSE_OK)
dialog.set_size_request(400, 400)
label = gtk.Label(title or _('Your selection:'))
dialog.vbox.pack_start(label, False, False)
dialog.vbox.pack_start(gtk.HSeparator(), False, True)
scrolledwindow = gtk.ScrolledWindow()
scrolledwindow.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
treeview = gtk.TreeView()
treeview.set_headers_visible(False)
scrolledwindow.add(treeview)
dialog.vbox.pack_start(scrolledwindow, True, True)
treeview.get_selection().set_mode('single')
cell = gtk.CellRendererText()
column = gtk.TreeViewColumn("Widget", cell, text=0)
treeview.append_column(column)
treeview.set_search_column(0)
model = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_INT)
keys = values.keys()
keys.sort()
i = 0
for val in keys:
model.append([str(val), i])
i += 1
treeview.set_model(model)
treeview.connect('row-activated',
lambda x, y, z: dialog.response(gtk.RESPONSE_OK) or True)
dialog.show_all()
response = dialog.run()
res = None
if response == gtk.RESPONSE_OK:
sel = treeview.get_selection().get_selected()
if sel:
(model, i) = sel
if i:
index = model.get_value(i, 1)
value = keys[index]
res = (value, values[value])
parent.present()
dialog.destroy()
return res
def file_selection(title, filename='',
action=gtk.FILE_CHOOSER_ACTION_OPEN, preview=True, multi=False,
filters=None):
parent = get_toplevel_window()
if action == gtk.FILE_CHOOSER_ACTION_OPEN:
buttons = (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
gtk.STOCK_OPEN, gtk.RESPONSE_OK)
else:
buttons = (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
gtk.STOCK_SAVE, gtk.RESPONSE_OK)
win = gtk.FileChooserDialog(title, None, action, buttons)
win.set_transient_for(parent)
win.set_icon(TRYTON_ICON)
win.set_current_folder(CONFIG['client.default_path'])
if filename:
win.set_current_name(filename)
win.set_select_multiple(multi)
win.set_default_response(gtk.RESPONSE_OK)
if filters is not None:
for filt in filters:
win.add_filter(filt)
def update_preview_cb(win, img):
have_preview = False
filename = win.get_preview_filename()
if filename:
try:
pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(filename.decode(
sys.getfilesystemencoding().encode('utf-8')), 128, 128)
img.set_from_pixbuf(pixbuf)
have_preview = True
except (IOError, glib.GError):
pass
win.set_preview_widget_active(have_preview)
return
if preview:
img_preview = gtk.Image()
win.set_preview_widget(img_preview)
win.connect('update-preview', update_preview_cb, img_preview)
button = win.run()
if button != gtk.RESPONSE_OK:
parent.present()
win.destroy()
return False
if not multi:
filepath = win.get_filename()
if filepath:
filepath = filepath.decode('utf-8')
try:
CONFIG['client.default_path'] = \
os.path.dirname(filepath)
CONFIG.save()
except IOError:
pass
parent.present()
win.destroy()
return filepath
else:
filenames = win.get_filenames()
if filenames:
filenames = [x.decode('utf-8') for x in filenames]
try:
CONFIG['client.default_path'] = \
os.path.dirname(filenames[0])
except IOError:
pass
parent.present()
win.destroy()
return filenames
def file_open(filename, type, print_p=False):
def save():
save_name = file_selection(_('Save As...'),
action=gtk.FILE_CHOOSER_ACTION_SAVE)
if save_name:
file_p = open(filename, 'rb')
save_p = open(save_name, 'wb+')
save_p.write(file_p.read())
save_p.close()
file_p.close()
if os.name == 'nt':
operation = 'open'
if print_p:
operation = 'print'
try:
os.startfile(os.path.normpath(filename), operation)
except WindowsError:
save()
elif sys.platform == 'darwin':
try:
subprocess.Popen(['/usr/bin/open', filename])
except OSError:
save()
else:
try:
subprocess.Popen(['xdg-open', filename])
except OSError:
save()
def mailto(to=None, cc=None, subject=None, body=None, attachment=None):
if CONFIG['client.email']:
cmd = Template(CONFIG['client.email']).substitute(
to=to or '',
cc=cc or '',
subject=subject or '',
body=body or '',
attachment=attachment or '',
)
args = shlex.split(str(cmd))
subprocess.Popen(args)
return
if os.name != 'nt' and sys.platform != 'darwin':
args = ['xdg-email', '--utf8']
if cc:
args.extend(['--cc', cc])
if subject:
args.extend(['--subject', subject])
if body:
args.extend(['--body', body])
if attachment:
args.extend(['--attach', attachment])
if to:
args.append(to)
try:
subprocess.Popen(args)
return
except OSError:
pass
#http://www.faqs.org/rfcs/rfc2368.html
url = "mailto:"
if to:
if isinstance(to, unicode):
to = to.encode('utf-8')
url += urllib.quote(to.strip(), "@,")
url += '?'
if cc:
if isinstance(cc, unicode):
cc = cc.encode('utf-8')
url += "&cc=" + urllib.quote(cc, "@,")
if subject:
if isinstance(subject, unicode):
subject = subject.encode('utf-8')
url += "&subject=" + urllib.quote(subject, "")
if body:
if isinstance(body, unicode):
body = body.encode('utf-8')
body = "\r\n".join(body.splitlines())
url += "&body=" + urllib.quote(body, "")
if attachment:
if isinstance(attachment, unicode):
attachment = attachment.encode('utf-8')
url += "&attachment=" + urllib.quote(attachment, "")
webbrowser.open(url, new=1)
class UniqueDialog(object):
def __init__(self):
self.running = False
def build_dialog(self, *args):
raise NotImplementedError
def __call__(self, *args):
if self.running:
return
parent = get_toplevel_window()
dialog = self.build_dialog(parent, *args)
dialog.set_icon(TRYTON_ICON)
self.running = True
dialog.show_all()
response = dialog.run()
parent.present()
dialog.destroy()
self.running = False
return response
class MessageDialog(UniqueDialog):
def build_dialog(self, parent, message, msg_type):
dialog = gtk.MessageDialog(parent,
gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, msg_type,
gtk.BUTTONS_OK, message)
return dialog
def __call__(self, message, msg_type=gtk.MESSAGE_INFO):
super(MessageDialog, self).__call__(message, msg_type)
message = MessageDialog()
class WarningDialog(UniqueDialog):
def build_dialog(self, parent, message, title, buttons=gtk.BUTTONS_OK):
dialog = gtk.MessageDialog(parent, gtk.DIALOG_DESTROY_WITH_PARENT,
gtk.MESSAGE_WARNING, buttons)
if hasattr(dialog, 'format_secondary_markup'):
dialog.set_markup('<b>%s</b>' % (to_xml(title)))
dialog.format_secondary_markup(to_xml(message))
else:
dialog.set_markup(
'<b>%s</b>\n%s' % (to_xml(title), to_xml(message)))
return dialog
warning = WarningDialog()
class UserWarningDialog(WarningDialog):
def __init__(self):
super(UserWarningDialog, self).__init__()
self.always = False
def _set_always(self, toggle):
self.always = toggle.get_active()
def build_dialog(self, parent, message, title):
dialog = super(UserWarningDialog, self).build_dialog(parent, message,
title, gtk.BUTTONS_YES_NO)
check = gtk.CheckButton(_('Always ignore this warning.'))
check.connect_after('toggled', self._set_always)
alignment = gtk.Alignment(0, 0.5)
alignment.add(check)
dialog.vbox.pack_start(alignment, True, False)
label = gtk.Label(_('Do you want to proceed?'))
label.set_alignment(1, 0.5)
dialog.vbox.pack_start(label, True, True)
return dialog
def __call__(self, message, title):
response = super(UserWarningDialog, self).__call__(message, title)
if response == gtk.RESPONSE_YES:
if self.always:
return 'always'
return 'ok'
return 'cancel'
userwarning = UserWarningDialog()
class ConfirmationDialog(UniqueDialog):
def build_dialog(self, parent, message):
dialog = gtk.Dialog(_('Confirmation'), parent, gtk.DIALOG_MODAL
| gtk.DIALOG_DESTROY_WITH_PARENT | gtk.WIN_POS_CENTER_ON_PARENT
| gtk.gdk.WINDOW_TYPE_HINT_DIALOG)
dialog.set_has_separator(True)
hbox = gtk.HBox()
image = gtk.Image()
image.set_from_stock('tryton-dialog-information',
gtk.ICON_SIZE_DIALOG)
image.set_padding(15, 15)
hbox.pack_start(image, False, False)
label = gtk.Label('%s' % (to_xml(message)))
hbox.pack_start(label, True, True)
dialog.vbox.pack_start(hbox)
return dialog
class SurDialog(ConfirmationDialog):
def build_dialog(self, parent, message):
dialog = super(SurDialog, self).build_dialog(parent, message)
dialog.add_button("gtk-cancel", gtk.RESPONSE_CANCEL)
dialog.add_button("gtk-ok", gtk.RESPONSE_OK | gtk.CAN_DEFAULT
| gtk.HAS_DEFAULT)
dialog.set_default_response(gtk.RESPONSE_OK)
return dialog
def __call__(self, message):
response = super(SurDialog, self).__call__(message)
return response == gtk.RESPONSE_OK
sur = SurDialog()
class Sur3BDialog(ConfirmationDialog):
response_mapping = {
gtk.RESPONSE_YES: 'ok',
gtk.RESPONSE_NO: 'ko',
gtk.RESPONSE_CANCEL: 'cancel'
}
def build_dialog(self, parent, message):
dialog = super(Sur3BDialog, self).build_dialog(parent, message)
dialog.add_button("gtk-cancel", gtk.RESPONSE_CANCEL)
dialog.add_button("gtk-no", gtk.RESPONSE_NO)
dialog.add_button("gtk-yes", gtk.RESPONSE_YES | gtk.CAN_DEFAULT
| gtk.HAS_DEFAULT)
dialog.set_default_response(gtk.RESPONSE_YES)
return dialog
def __call__(self, message):
response = super(Sur3BDialog, self).__call__(message)
return self.response_mapping.get(response, 'cancel')
sur_3b = Sur3BDialog()
class AskDialog(UniqueDialog):
def build_dialog(self, parent, question, visibility):
win = gtk.Dialog('Tryton', parent,
gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
gtk.STOCK_OK, gtk.RESPONSE_OK))
win.set_has_separator(True)
win.set_default_response(gtk.RESPONSE_OK)
hbox = gtk.HBox()
image = gtk.Image()
image.set_from_stock('tryton-dialog-information',
gtk.ICON_SIZE_DIALOG)
hbox.pack_start(image)
vbox = gtk.VBox()
vbox.pack_start(gtk.Label(question))
self.entry = gtk.Entry()
self.entry.set_activates_default(True)
self.entry.set_visibility(visibility)
vbox.pack_start(self.entry)
hbox.pack_start(vbox)
win.vbox.pack_start(hbox)
return win
def __call__(self, question, visibility=True):
if self.running:
return
parent = get_toplevel_window()
dialog = self.build_dialog(parent, question, visibility=visibility)
dialog.set_icon(TRYTON_ICON)
self.running = True
dialog.show_all()
response = dialog.run()
result = None
if response == gtk.RESPONSE_OK:
result = self.entry.get_text()
parent.present()
dialog.destroy()
self.running = False
return result
ask = AskDialog()
class ConcurrencyDialog(UniqueDialog):
def build_dialog(self, parent, resource, obj_id, context):
dialog = gtk.Dialog(_('Concurrency Exception'), parent,
gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT |
gtk.WIN_POS_CENTER_ON_PARENT | gtk.gdk.WINDOW_TYPE_HINT_DIALOG)
dialog.set_has_separator(True)
dialog.set_default_response(gtk.RESPONSE_CANCEL)
hbox = gtk.HBox()
image = gtk.Image()
image.set_from_stock('tryton-dialog-information',
gtk.ICON_SIZE_DIALOG)
image.set_padding(15, 15)
hbox.pack_start(image, False, False)
label = gtk.Label()
label.set_padding(15, 15)
label.set_use_markup(True)
label.set_markup(_('<b>Write Concurrency Warning:</b>\n\n'
'This record has been modified while you were editing it.\n'
' Choose:\n'
' - "Cancel" to cancel saving;\n'
' - "Compare" to see the modified version;\n'
' - "Write Anyway" to save your current version.'))
hbox.pack_start(label, True, True)
dialog.vbox.pack_start(hbox)
dialog.add_button('gtk-cancel', gtk.RESPONSE_CANCEL)
compare_button = gtk.Button(_('Compare'))
image = gtk.Image()
image.set_from_stock('tryton-find-replace', gtk.ICON_SIZE_BUTTON)
compare_button.set_image(image)
dialog.add_action_widget(compare_button, gtk.RESPONSE_APPLY)
write_button = gtk.Button(_('Write Anyway'))
image = gtk.Image()
image.set_from_stock('tryton-save', gtk.ICON_SIZE_BUTTON)
write_button.set_image(image)
dialog.add_action_widget(write_button, gtk.RESPONSE_OK)
return dialog
def __call__(self, resource, obj_id, context):
res = super(ConcurrencyDialog, self).__call__(resource, obj_id,
context)
if res == gtk.RESPONSE_OK:
return True
if res == gtk.RESPONSE_APPLY:
from tryton.gui.window import Window
Window.create(False, resource, res_id=obj_id,
domain=[('id', '=', obj_id)],
context=context, mode=['form', 'tree'])
return False
concurrency = ConcurrencyDialog()
class ErrorDialog(UniqueDialog):
def build_dialog(self, parent, title, details):
dialog = gtk.Dialog(_('Error'), parent,
gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT)
dialog.set_has_separator(True)
but_send = gtk.Button(_('Report Bug'))
dialog.add_action_widget(but_send, gtk.RESPONSE_OK)
dialog.add_button("gtk-close", gtk.RESPONSE_CANCEL)
dialog.set_default_response(gtk.RESPONSE_CANCEL)
vbox = gtk.VBox()
label_title = gtk.Label()
label_title.set_markup('<b>' + _('Application Error!') + '</b>')
label_title.set_padding(-1, 5)
vbox.pack_start(label_title, False, False)
vbox.pack_start(gtk.HSeparator(), False, False)
hbox = gtk.HBox()
image = gtk.Image()
image.set_from_stock('tryton-dialog-error', gtk.ICON_SIZE_DIALOG)
hbox.pack_start(image, False, False)
scrolledwindow = gtk.ScrolledWindow()
scrolledwindow.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
scrolledwindow.set_shadow_type(gtk.SHADOW_NONE)
viewport = gtk.Viewport()
viewport.set_shadow_type(gtk.SHADOW_NONE)
box = gtk.VBox()
label_error = gtk.Label()
label_error.set_markup('')
label_error.set_alignment(0, 0.5)
label_error.set_padding(-1, 14)
label_error.modify_font(pango.FontDescription("monospace"))
label_error.set_markup('<b>' + _('Error: ') + '</b>' + to_xml(title))
box.pack_start(label_error, False, False)
textview = gtk.TextView()
buf = gtk.TextBuffer()
buf.set_text(details)
textview.set_buffer(buf)
textview.set_editable(False)
textview.set_sensitive(True)
textview.modify_font(pango.FontDescription("monospace"))
box.pack_start(textview, False, False)
viewport.add(box)
scrolledwindow.add(viewport)
hbox.pack_start(scrolledwindow)
vbox.pack_start(hbox)
button_roundup = gtk.Button()
button_roundup.set_relief(gtk.RELIEF_NONE)
label_roundup = gtk.Label()
label_roundup.set_markup(_('To report bugs you must have an account'
' on <u>%s</u>') % CONFIG['roundup.url'])
label_roundup.set_alignment(1, 0.5)
label_roundup.set_padding(20, 5)
button_roundup.connect('clicked',
lambda widget: webbrowser.open(CONFIG['roundup.url'], new=2))
button_roundup.add(label_roundup)
vbox.pack_start(button_roundup, False, False)
dialog.vbox.pack_start(vbox)
dialog.set_size_request(600, 400)
return dialog
def __call__(self, title, details):
if title == details:
title = ''
log = logging.getLogger(__name__)
log.error(details + '\n' + title)
response = super(ErrorDialog, self).__call__(title, details)
if response == gtk.RESPONSE_OK:
send_bugtracker(title, details)
error = ErrorDialog()
def send_bugtracker(title, msg):
from tryton import rpc
parent = get_toplevel_window()
win = gtk.Dialog(_('Bug Tracker'), parent,
gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
gtk.STOCK_OK, gtk.RESPONSE_OK))
win.set_icon(TRYTON_ICON)
win.set_default_response(gtk.RESPONSE_OK)
win.set_has_separator(True)
hbox = gtk.HBox()
image = gtk.Image()
image.set_from_stock('tryton-dialog-information',
gtk.ICON_SIZE_DIALOG)
hbox.pack_start(image, False, False)
table = gtk.Table(2, 2)
table.set_col_spacings(3)
table.set_row_spacings(3)
table.set_border_width(1)
label_user = gtk.Label(_('User:'))
label_user.set_alignment(1.0, 0.5)
table.attach(label_user, 0, 1, 0, 1, yoptions=False,
xoptions=gtk.FILL)
entry_user = gtk.Entry()
entry_user.set_activates_default(True)
table.attach(entry_user, 1, 2, 0, 1, yoptions=False,
xoptions=gtk.FILL)
label_password = gtk.Label(_('Password:'))
label_password.set_alignment(1.0, 0.5)
table.attach(label_password, 0, 1, 1, 2, yoptions=False,
xoptions=gtk.FILL)
entry_password = gtk.Entry()
entry_password.set_activates_default(True)
entry_password.set_visibility(False)
table.attach(entry_password, 1, 2, 1, 2, yoptions=False,
xoptions=gtk.FILL)
hbox.pack_start(table)
win.vbox.pack_start(hbox)
win.show_all()
if rpc._USERNAME:
entry_user.set_text(rpc._USERNAME)
entry_password.grab_focus()
else:
entry_user.grab_focus()
response = win.run()
parent.present()
user = entry_user.get_text()
password = entry_password.get_text()
win.destroy()
if response == gtk.RESPONSE_OK:
try:
msg = msg.encode('ascii', 'replace')
protocol = 'http'
if ssl or hasattr(socket, 'ssl'):
protocol = 'https'
server = xmlrpclib.Server(
('%s://%s:%s@' + CONFIG['roundup.xmlrpc'])
% (protocol, user, password), allow_none=True)
if hashlib:
msg_md5 = hashlib.md5(msg + '\n' + title).hexdigest()
else:
msg_md5 = md5.new(msg + '\n' + title).hexdigest()
if not title:
title = '[no title]'
issue_id = None
msg_ids = server.filter('msg', None, {'summary': str(msg_md5)})
if msg_ids:
issue_ids = server.filter('issue', None, {'messages': msg_ids})
if issue_ids:
issue_id = issue_ids[0]
if issue_id:
# issue to same message already exists, add user to nosy-list
server.set('issue' + str(issue_id), *['nosy=+' + user])
message(
_('The same bug was already reported by another user.\n'
'To keep you informed your username is added '
'to the nosy-list of this issue') + '%s' % issue_id)
else:
# create a new issue for this error-message
# first create message
msg_id = server.create('msg', *['content=' + msg,
'author=' + user, 'summary=' + msg_md5])
# second create issue with this message
issue_id = server.create('issue', *['messages=' + str(msg_id),
'nosy=' + user, 'title=' + title, 'priority=bug'])
message(_('Created new bug with ID ') + \
'issue%s' % issue_id)
webbrowser.open(CONFIG['roundup.url'] + 'issue%s' % issue_id,
new=2)
except (socket.error, xmlrpclib.Fault), exception:
if (isinstance(exception, xmlrpclib.Fault)
and 'roundup.cgi.exceptions.Unauthorised' in
exception.faultString):
message(_('Connection error!\nBad username or password!'))
return send_bugtracker(title, msg)
tb_s = reduce(lambda x, y: x + y,
traceback.format_exception(sys.exc_type,
sys.exc_value, sys.exc_traceback))
message(_('Exception:') + '\n' + tb_s, msg_type=gtk.MESSAGE_ERROR)
def to_xml(string):
return string.replace('&', '&amp;'
).replace('<', '&lt;').replace('>', '&gt;')
PLOCK = Lock()
def process_exception(exception, *args, **kwargs):
if isinstance(exception, TrytonError):
if exception.faultCode == 'BadFingerprint':
warning(
_('The server fingerprint has changed since last connection!\n'
'The application will stop connecting to this server '
'until its fingerprint is fixed.'), _('Security risk!'))
from tryton.gui.main import Main
Main.sig_quit()
sys.exit()
elif exception.faultCode == 'NotLogged':
if rpc.CONNECTION is None:
message(_('Connection error!\n' \
'Unable to connect to the server!'))
return False
elif isinstance(exception, TrytonServerError):
if exception.faultCode == 'UserWarning':
name, msg, description = exception.args
res = userwarning(description, msg)
if res in ('always', 'ok'):
args2 = ('model', 'res.user.warning', 'create', {
'user': rpc._USER,
'name': name,
'always': (res == 'always'),
}, rpc.CONTEXT)
try:
rpc.execute(*args2)
except TrytonServerError, exception:
process_exception(exception, *args2)
if args:
try:
return rpc.execute(*args)
except TrytonServerError, exception:
return process_exception(exception, *args)
return True
return False
elif exception.faultCode == 'UserError':
msg, description = exception.args
warning(description, msg)
return False
elif exception.faultCode == 'ConcurrencyException':
if len(args) >= 6:
if concurrency(args[1], args[3][0], args[5]):
if '_timestamp' in args[5]:
del args[5]['_timestamp']
try:
return rpc.execute(*args)
except TrytonServerError, exception:
return process_exception(exception, *args)
return False
else:
message(_('Concurrency Exception'), msg_type=gtk.MESSAGE_ERROR)
return False
elif exception.faultCode == 'NotLogged':
from tryton.gui.main import Main
if not PLOCK.acquire(False):
return False
hostname = rpc._HOST
port = rpc._PORT
try:
while True:
password = ask(_('Password:'), visibility=False)
if password is None:
Main.get_main().sig_logout()
return False
res = rpc.login(rpc._USERNAME, password, hostname, port,
rpc._DATABASE)
Main.get_main().refresh_ssl()
if res == -1:
message(_('Connection error!\n' \
'Unable to connect to the server!'))
return False
if res < 0:
continue
if args:
try:
return rpc.execute(*args)
except TrytonServerError, exception:
return process_exception(exception, *args)
return True
finally:
PLOCK.release()
elif isinstance(exception, (socket.error, TrytonServerUnavailable)):
warning(str(exception), _('Network Error!'))
return False
if isinstance(exception, TrytonServerError):
error_title, error_detail = exception.faultCode, exception.faultString
else:
error_title = str(exception)
if 'tb' in kwargs:
error_detail = kwargs['tb']
else:
error_detail = traceback.format_exc()
error(error_title, error_detail)
return False
def node_attributes(node):
result = {}
attrs = node.attributes
if attrs is None:
return {}
for i in range(attrs.length):
result[str(attrs.item(i).localName)] = str(attrs.item(i).nodeValue)
return result
def hex2rgb(hexstring, digits=2):
"""
Converts a hexstring color to a rgb tuple.
Example: #ff0000 -> (1.0, 0.0, 0.0)
digits is an integer number telling how many characters should be
interpreted for each component in the hexstring.
"""
if isinstance(hexstring, (tuple, list)):
return hexstring
top = float(int(digits * 'f', 16))
r = int(hexstring[1:digits + 1], 16)
g = int(hexstring[digits + 1:digits * 2 + 1], 16)
b = int(hexstring[digits * 2 + 1:digits * 3 + 1], 16)
return r / top, g / top, b / top
def clamp(minValue, maxValue, value):
"""Make sure value is between minValue and maxValue"""
if value < minValue:
return minValue
if value > maxValue:
return maxValue
return value
def lighten(r, g, b, amount):
"""Return a lighter version of the color (r, g, b)"""
return (clamp(0.0, 1.0, r + amount),
clamp(0.0, 1.0, g + amount),
clamp(0.0, 1.0, b + amount))
def generateColorscheme(masterColor, keys, light=0.06):
"""
Generates a dictionary where the keys match the keys argument and
the values are colors derivated from the masterColor.
Each color is a lighter version of masterColor separated by a difference
given by the light argument.
The masterColor is given in a hex string format.
"""
r, g, b = hex2rgb(COLOR_SCHEMES.get(masterColor, masterColor))
return dict([(key, lighten(r, g, b, light * i))
for i, key in enumerate(keys)])
class DBProgress(object):
def __init__(self, host, port):
self.dbs, self.createdb = None, None
self.host, self.port = host, port
self.updated = threading.Event()
self.db_info = None
def start(self):
dbs, createdb = None, False
try:
dbs = refresh_dblist(self.host, self.port)
createdb = True
except Exception:
pass
finally:
self.db_info = (dbs, createdb)
self.updated.set()
def update(self, combo, progressbar, callback, dbname=''):
self.db_info = None
threading.Thread(target=self.start).start()
gobject.timeout_add(100, self.end, combo, progressbar, callback,
dbname)
def end(self, combo, progressbar, callback, dbname):
if not self.updated.isSet():
progressbar.show()
progressbar.pulse()
return True
progressbar.hide()
dbs, createdb = self.db_info
if dbs is None:
dbs, createdb = None, False
elif dbs == -1:
dbs, createdb = -1, False
else:
from tryton.gui.main import Main
Main.get_main().refresh_ssl()
liststore = combo.get_model()
liststore.clear()
index = -1
for db_num, db_name in enumerate(dbs):
liststore.append([db_name])
if db_name == dbname:
index = db_num
if index == -1:
index = 0
combo.set_active(index)
dbs = len(dbs)
callback(dbs, createdb)
return False
class RPCException(Exception):
def __init__(self, exception):
self.exception = exception
class RPCProgress(object):
def __init__(self, method, args):
self.method = method
self.args = args
self.parent = get_toplevel_window()
self.res = None
self.error = False
self.exception = None
def start(self):
try:
self.res = getattr(rpc, self.method)(*self.args)
except Exception, exception:
self.error = True
self.res = False
self.exception = exception
return True
if not self.res:
self.error = True
return True
def run(self, process_exception_p=True, main_iteration_p=True):
thread.start_new_thread(self.start, ())
watch = None
i = 0
while (not self.res) and (not self.error):
i += 1
if i > 1:
watch = gtk.gdk.Cursor(gtk.gdk.WATCH)
self.parent.window.set_cursor(watch)
if main_iteration_p:
with gtk.gdk.lock:
while gtk.events_pending():
gtk.main_iteration()
time.sleep(0.1)
if self.parent.window:
self.parent.window.set_cursor(None)
if self.exception:
if process_exception_p:
if process_exception(self.exception):
self.res = None
self.error = False
self.exception = None
return self.run(process_exception_p)
raise RPCException(self.exception)
else:
raise self.exception
return self.res
def RPCExecute(*args, **kwargs):
rpc_context = rpc.CONTEXT.copy()
if kwargs.get('context'):
rpc_context.update(kwargs['context'])
args = args + (rpc_context,)
process_exception = kwargs.get('process_exception', True)
main_iteration = kwargs.get('main_iteration', True)
return RPCProgress('execute', args).run(process_exception, main_iteration)
class Tooltips(object):
_tooltips = None
def set_tip(self, widget, tip_text):
if hasattr(widget, 'set_tooltip_text'):
return widget.set_tooltip_text(tip_text)
if not self._tooltips:
self._tooltips = gtk.Tooltips()
return self._tooltips.set_tip(widget, tip_text)
def enable(self):
if self._tooltips:
self._tooltips.enable()
def disable(self):
if self._tooltips:
self._tooltips.disable()
COLOR_SCHEMES = {
'red': '#cf1d1d',
'green': '#3fb41b',
'blue': '#224565',
'grey': '#444444',
'black': '#000000',
'darkcyan': '#305755',
}
COLORS = {
'invalid': '#ff6969',
'required': '#d2d2ff',
}
FLOAT_TIME_CONV = {
'Y': 8760,
'M': 672,
'w': 168,
'd': 24,
'h': 1,
'm': 1.0 / 60,
}
FLOAT_TIME_SEPS = {
'Y': _('Y'),
'M': _('M'),
'w': _('w'),
'd': _('d'),
'h': _('h'),
'm': _('m'),
}
def text_to_float_time(text, conv=None, digit=2):
if not text:
return None
try:
try:
return round(locale.atof(text), digit)
except ValueError:
pass
if conv:
tmp_conv = FLOAT_TIME_CONV.copy()
tmp_conv.update(conv)
conv = tmp_conv
else:
conv = FLOAT_TIME_CONV
for key in FLOAT_TIME_SEPS.keys():
text = text.replace(FLOAT_TIME_SEPS[key], key + ' ')
value = 0
for buf in text.split(' '):
buf = buf.strip()
if ':' in buf:
hour, min = buf.split(':')
value += abs(int(hour or 0))
value += abs(int(min or 0) * conv['m'])
continue
elif '-' in buf and not buf.startswith('-'):
hour, min = buf.split('-')
value += abs(int(hour or 0))
value += abs(int(min or 0) * conv['m'])
continue
try:
value += abs(locale.atof(buf))
continue
except ValueError:
pass
for sep in conv.keys():
if buf.endswith(sep):
value += abs(locale.atof(buf[:-len(sep)])) * conv[sep]
break
if text.startswith('-'):
value *= -1
return round(value, digit)
except ValueError:
return 0.0
def float_time_to_text(val, conv=None):
if val is None:
return ''
if conv:
tmp_conv = FLOAT_TIME_CONV.copy()
tmp_conv.update(conv)
conv = tmp_conv
else:
conv = FLOAT_TIME_CONV
value = ''
if val < 0:
value += '-'
val = abs(val)
years = int(val / conv['Y'])
val = val - years * conv['Y']
months = int(val / conv['M'])
val = val - months * conv['M']
weeks = int(val / conv['w'])
val = val - weeks * conv['w']
days = int(val / conv['d'])
val = val - days * conv['d']
hours = int(val)
val = val - hours
mins = int((val % 1 + 0.01) / conv['m'])
if years:
value += ' ' + locale.format('%d', years, True) + FLOAT_TIME_SEPS['Y']
if months:
value += ' ' + locale.format('%d', months, True) + FLOAT_TIME_SEPS['M']
if weeks:
value += ' ' + locale.format('%d', weeks, True) + FLOAT_TIME_SEPS['w']
if days:
value += ' ' + locale.format('%d', days, True) + FLOAT_TIME_SEPS['d']
if hours or mins or not value:
value += ' %02d:%02d' % (hours, mins)
value = value.strip()
return value
def filter_domain(domain):
'''
Return the biggest subset of domain with only AND operator
'''
res = []
for arg in domain:
if isinstance(arg, basestring):
if arg == 'OR':
res = []
break
continue
if isinstance(arg, tuple):
res.append(arg)
elif isinstance(arg, list):
res.extend(filter_domain(arg))
return res
_ALLOWED_CODES = set(dis.opmap[x] for x in [
'POP_TOP', 'ROT_TWO', 'ROT_THREE', 'ROT_FOUR', 'DUP_TOP', 'BUILD_LIST',
'BUILD_MAP', 'BUILD_TUPLE', 'LOAD_CONST', 'RETURN_VALUE',
'STORE_SUBSCR', 'UNARY_POSITIVE', 'UNARY_NEGATIVE', 'UNARY_NOT',
'UNARY_INVERT', 'BINARY_POWER', 'BINARY_MULTIPLY', 'BINARY_DIVIDE',
'BINARY_FLOOR_DIVIDE', 'BINARY_TRUE_DIVIDE', 'BINARY_MODULO',
'BINARY_ADD', 'BINARY_SUBTRACT', 'BINARY_LSHIFT', 'BINARY_RSHIFT',
'BINARY_AND', 'BINARY_XOR', 'BINARY_OR', 'STORE_MAP', 'LOAD_NAME',
'CALL_FUNCTION', 'COMPARE_OP', 'LOAD_ATTR', 'STORE_NAME', 'GET_ITER',
'FOR_ITER', 'LIST_APPEND', 'JUMP_ABSOLUTE', 'DELETE_NAME',
'JUMP_IF_TRUE', 'JUMP_IF_FALSE', 'JUMP_IF_FALSE_OR_POP',
'JUMP_IF_TRUE_OR_POP', 'POP_JUMP_IF_FALSE', 'POP_JUMP_IF_TRUE',
'BINARY_SUBSCR',
] if x in dis.opmap)
_SAFE_EVAL_CACHE = {}
def safe_eval(source, data=None):
if '__subclasses__' in source:
raise ValueError('__subclasses__ not allowed')
if hashlib:
key = hashlib.md5(source).digest()
else:
key = md5.new(source).digest()
c = _SAFE_EVAL_CACHE.get(key)
if not c:
c = compile(source, '', 'eval')
codes = []
s = c.co_code
i = 0
while i < len(s):
code = ord(s[i])
codes.append(code)
if code >= dis.HAVE_ARGUMENT:
i += 3
else:
i += 1
for code in codes:
if code not in _ALLOWED_CODES:
raise ValueError('opcode %s not allowed' % dis.opname[code])
if len(_SAFE_EVAL_CACHE) > 1024:
_SAFE_EVAL_CACHE.clear()
_SAFE_EVAL_CACHE[key] = c
return eval(c, {'__builtins__': {
'True': True,
'False': False,
'str': str,
'globals': locals,
'locals': locals,
'bool': bool,
'dict': dict,
}}, data)
def timezoned_date(date, reverse=False):
if pytz and rpc.CONTEXT.get('timezone') and rpc.TIMEZONE:
lzone = pytz.timezone(rpc.CONTEXT['timezone'])
szone = pytz.timezone(rpc.TIMEZONE)
if reverse:
lzone, szone = szone, lzone
sdt = szone.localize(date, is_dst=True)
ldt = sdt.astimezone(lzone)
date = ldt
return date
def untimezoned_date(date):
return timezoned_date(date, reverse=True).replace(tzinfo=None)
def humanize(size):
for x in ('bytes', 'KB', 'MB', 'GB', 'TB', 'PB'):
if size < 1000:
return '%3.1f%s' % (size, x)
size /= 1000.0