Add option to show revisions
issue3678 review3501003
This commit is contained in:
1 parent
61b815d787
commit
086b4ae1ae
12 files changed
+177
-37
No files matched your search
@@ -1,3 +1,4 @@
|
||||
* Add option to show revisions
|
||||
* Add a multi selection widget for many2many
|
||||
* Remove auto-refresh
|
||||
* Add support of domain for non-relation field
|
||||
|
||||
@@ -113,6 +113,7 @@ class Action(object):
|
||||
action_ctx = decoder.decode(action.get('pyson_context') or '{}')
|
||||
ctx.update(action_ctx)
|
||||
ctx.update(context)
|
||||
action_ctx.update(context)
|
||||
|
||||
ctx['context'] = ctx
|
||||
decoder = PYSONDecoder(ctx)
|
||||
|
||||
@@ -168,6 +168,23 @@ class ModelAccess(object):
|
||||
MODELACCESS = ModelAccess()
|
||||
|
||||
|
||||
class ModelHistory(object):
|
||||
_models = set()
|
||||
|
||||
def load_history(self):
|
||||
self._models.clear()
|
||||
try:
|
||||
self._models.update(rpc.execute('model', 'ir.model',
|
||||
'list_history', rpc.CONTEXT))
|
||||
except TrytonServerError:
|
||||
pass
|
||||
|
||||
def __contains__(self, model):
|
||||
return model in self._models
|
||||
|
||||
MODELHISTORY = ModelHistory()
|
||||
|
||||
|
||||
class ViewSearch(object):
|
||||
searches = None
|
||||
|
||||
|
||||
@@ -154,7 +154,11 @@ class DateEntry(gtk.Entry):
|
||||
def set_text(self, text):
|
||||
self._interactive_input = False
|
||||
try:
|
||||
# Force position as _on_insert_text use it
|
||||
position = self.get_position()
|
||||
self.set_position(0)
|
||||
gtk.Entry.set_text(self, text)
|
||||
self.idle_set_position(position)
|
||||
finally:
|
||||
self._interactive_input = True
|
||||
|
||||
@@ -217,7 +221,7 @@ class DateEntry(gtk.Entry):
|
||||
res = None
|
||||
date = self.compute_date(self.get_text())
|
||||
try:
|
||||
res = datetime.datetime(*time.strptime(date, self.format)[:6])
|
||||
res = datetime.datetime.strptime(date, self.format)
|
||||
except ValueError:
|
||||
return None
|
||||
if set_text:
|
||||
|
||||
@@ -855,6 +855,7 @@ class Main(object):
|
||||
for target in (
|
||||
common.ICONFACTORY.load_icons,
|
||||
common.MODELACCESS.load_models,
|
||||
common.MODELHISTORY.load_history,
|
||||
common.VIEW_SEARCH.load_searches,
|
||||
):
|
||||
t = threading.Thread(target=target)
|
||||
|
||||
@@ -15,11 +15,12 @@ class Attachment(WinForm):
|
||||
def __init__(self, record, callback=None):
|
||||
self.resource = '%s,%s' % (record.model_name, record.id)
|
||||
self.attachment_callback = callback
|
||||
context = record.context_get()
|
||||
context['resource'] = self.resource
|
||||
screen = Screen('ir.attachment', domain=[
|
||||
('resource', '=', self.resource),
|
||||
], mode=['tree', 'form'], context={
|
||||
'resource': self.resource,
|
||||
}, exclude_field='resource')
|
||||
], mode=['tree', 'form'], context=context,
|
||||
exclude_field='resource')
|
||||
screen.search_filter()
|
||||
screen.parent = record
|
||||
super(Attachment, self).__init__(screen, self.callback,
|
||||
|
||||
+58
-18
@@ -11,11 +11,13 @@ from tryton.gui.window import Window
|
||||
from tryton.gui.window.win_export import WinExport
|
||||
from tryton.gui.window.win_import import WinImport
|
||||
from tryton.gui.window.attachment import Attachment
|
||||
from tryton.gui.window.revision import Revision
|
||||
from tryton.signal_event import SignalEvent
|
||||
from tryton.common import message, sur, sur_3b, COLOR_SCHEMES, timezoned_date
|
||||
import tryton.common as common
|
||||
from tryton.translate import date_format
|
||||
from tryton.common import RPCExecute, RPCException
|
||||
from tryton.common.datetime_strftime import datetime_strftime
|
||||
from tryton import plugins
|
||||
|
||||
from tabcontent import TabContent
|
||||
@@ -61,6 +63,7 @@ class Form(SignalEvent, TabContent):
|
||||
(_('_Next'), 'tryton-go-next', 'sig_next', '<tryton>/Form/Next'),
|
||||
(_('_Search'), 'tryton-find', 'sig_search', '<tryton>/Form/Search'),
|
||||
(_('View _Logs...'), None, 'sig_logs', None),
|
||||
(_('Show revisions...'), 'tryton-clock', 'revision', None),
|
||||
(None,) * 4,
|
||||
(_('_Close Tab'), 'tryton-close', 'sig_win_close',
|
||||
'<tryton>/Form/Close'),
|
||||
@@ -96,8 +99,6 @@ class Form(SignalEvent, TabContent):
|
||||
domain = []
|
||||
if view_ids is None:
|
||||
view_ids = []
|
||||
if context is None:
|
||||
context = {}
|
||||
|
||||
self.model = model
|
||||
self.res_id = res_id
|
||||
@@ -107,7 +108,7 @@ class Form(SignalEvent, TabContent):
|
||||
self.view_ids = view_ids
|
||||
self.dialogs = []
|
||||
|
||||
self.screen = Screen(self.model, mode=mode, context=self.context,
|
||||
self.screen = Screen(self.model, mode=mode, context=context,
|
||||
view_ids=view_ids, domain=domain, limit=limit, order=order,
|
||||
search_value=search_value, tab_domain=tab_domain)
|
||||
self.screen.widget.show()
|
||||
@@ -117,6 +118,12 @@ class Form(SignalEvent, TabContent):
|
||||
else:
|
||||
self.name = name
|
||||
|
||||
if self.model not in common.MODELHISTORY:
|
||||
self.menu_def = self.menu_def[:]
|
||||
# Remove callback to revision
|
||||
self.menu_def[11] = (self.menu_def[11][:2] + (None,)
|
||||
+ self.menu_def[11][3:])
|
||||
|
||||
self.create_tabcontent()
|
||||
|
||||
self.url_entry = url_entry = gtk.Entry()
|
||||
@@ -154,10 +161,12 @@ class Form(SignalEvent, TabContent):
|
||||
in ('tree', 'graph', 'calendar'):
|
||||
self.screen.search_filter()
|
||||
|
||||
self.update_revision()
|
||||
|
||||
def get_toolbars(self):
|
||||
try:
|
||||
return RPCExecute('model', self.model, 'view_toolbar_get',
|
||||
context=self.context)
|
||||
context=self.screen.context)
|
||||
except RPCException:
|
||||
return {}
|
||||
|
||||
@@ -174,7 +183,7 @@ class Form(SignalEvent, TabContent):
|
||||
and self.domain == value.domain
|
||||
and self.mode == value.mode
|
||||
and self.view_ids == value.view_ids
|
||||
and self.context == value.context
|
||||
and self.screen.context == value.screen.context
|
||||
and self.name == value.name
|
||||
and self.screen.limit == value.screen.limit
|
||||
and self.screen.search_value == value.screen.search_value)
|
||||
@@ -235,7 +244,7 @@ class Form(SignalEvent, TabContent):
|
||||
|
||||
try:
|
||||
res = RPCExecute('model', self.model, 'read', [obj_id],
|
||||
[x[0] for x in fields], context=self.context)
|
||||
[x[0] for x in fields], context=self.screen.context)
|
||||
except RPCException:
|
||||
return
|
||||
message_str = ''
|
||||
@@ -252,6 +261,42 @@ class Form(SignalEvent, TabContent):
|
||||
message(message_str)
|
||||
return True
|
||||
|
||||
def revision(self, widget=None):
|
||||
if not self.modified_save():
|
||||
return
|
||||
current_id = (self.screen.current_record.id
|
||||
if self.screen.current_record else None)
|
||||
try:
|
||||
revisions = RPCExecute('model', self.model, 'history_revisions',
|
||||
[r.id for r in self.screen.selected_records])
|
||||
except RPCException:
|
||||
return
|
||||
revision = self.screen.context.get('_datetime')
|
||||
revision = Revision(revisions, revision).run()
|
||||
if revision != self.screen.context.get('_datetime'):
|
||||
self.screen.clear()
|
||||
# Update root group context that will be propagated
|
||||
self.screen.group._context['_datetime'] = revision
|
||||
if self.screen.current_view.view_type != 'form':
|
||||
self.screen.search_filter(
|
||||
self.screen.screen_container.get_text())
|
||||
else:
|
||||
# Test if record exist in revisions
|
||||
self.screen.load([current_id])
|
||||
self.screen.display(set_cursor=True)
|
||||
self.update_revision()
|
||||
|
||||
def update_revision(self):
|
||||
revision = self.screen.context.get('_datetime')
|
||||
if revision:
|
||||
format_ = date_format() + ' %H:%M:%S.%f'
|
||||
revision = datetime_strftime(revision, format_)
|
||||
self.title.set_label('%s @ %s' % (self.name, revision))
|
||||
else:
|
||||
self.title.set_label(self.name)
|
||||
for button in ('new', 'save'):
|
||||
self.buttons[button].props.sensitive = not revision
|
||||
|
||||
def sig_remove(self, widget=None):
|
||||
if not common.MODELACCESS[self.model]['delete']:
|
||||
return
|
||||
@@ -271,7 +316,7 @@ class Form(SignalEvent, TabContent):
|
||||
fields = {}
|
||||
for name, field in self.screen.group.fields.iteritems():
|
||||
fields[name] = field.attrs
|
||||
WinImport(self.model, self.context)
|
||||
WinImport(self.model, self.screen.context)
|
||||
|
||||
def sig_save_as(self, widget=None):
|
||||
while self.screen.view_to_load:
|
||||
@@ -279,7 +324,7 @@ class Form(SignalEvent, TabContent):
|
||||
fields = {}
|
||||
for name, field in self.screen.group.fields.iteritems():
|
||||
fields[name] = field.attrs
|
||||
WinExport(self.model, self.ids_get(), context=self.context)
|
||||
WinExport(self.model, self.ids_get(), context=self.screen.context)
|
||||
|
||||
def sig_new(self, widget=None, autosave=True):
|
||||
if not common.MODELACCESS[self.model]['create']:
|
||||
@@ -296,13 +341,7 @@ class Form(SignalEvent, TabContent):
|
||||
return
|
||||
if not self.modified_save():
|
||||
return
|
||||
ids = [r.id for r in self.screen.selected_records]
|
||||
try:
|
||||
new_ids = RPCExecute('model', self.model, 'copy', ids, {},
|
||||
context=self.context)
|
||||
except RPCException:
|
||||
return
|
||||
self.screen.load(new_ids)
|
||||
self.screen.copy()
|
||||
self.message_info(_('Working now on the duplicated record(s)!'),
|
||||
'green')
|
||||
|
||||
@@ -336,11 +375,12 @@ class Form(SignalEvent, TabContent):
|
||||
self.screen.cancel_current()
|
||||
set_cursor = False
|
||||
self.screen.save_tree_state(store=False)
|
||||
record_id = (self.screen.current_record.id
|
||||
if self.screen.current_record else None)
|
||||
if self.screen.current_view.view_type != 'form':
|
||||
obj_id = self.id_get()
|
||||
self.screen.search_filter(self.screen.screen_container.get_text())
|
||||
for record in self.screen.group:
|
||||
if record.id == obj_id:
|
||||
if record.id == record_id:
|
||||
self.screen.current_record = record
|
||||
set_cursor = True
|
||||
break
|
||||
@@ -467,7 +507,7 @@ class Form(SignalEvent, TabContent):
|
||||
'id': record_id,
|
||||
'ids': record_ids,
|
||||
}
|
||||
Action._exec_action(action, data, self.context)
|
||||
Action._exec_action(action, data, self.screen.context)
|
||||
|
||||
def activate_save(self):
|
||||
self.buttons['save'].props.sensitive = self.screen.modified()
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
#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 gettext
|
||||
|
||||
from tryton.config import TRYTON_ICON
|
||||
from tryton.common import get_toplevel_window
|
||||
from tryton.common.date_widget import DateEntry
|
||||
from tryton.common.datetime_strftime import datetime_strftime
|
||||
from tryton.translate import date_format
|
||||
|
||||
_ = gettext.gettext
|
||||
|
||||
|
||||
class Revision(object):
|
||||
'Ask revision'
|
||||
|
||||
def __init__(self, revisions, revision=None):
|
||||
self.parent = get_toplevel_window()
|
||||
self.win = gtk.Dialog(_('Revision'), self.parent,
|
||||
gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
|
||||
(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
|
||||
gtk.STOCK_OK, gtk.RESPONSE_OK))
|
||||
self.win.set_default_response(gtk.RESPONSE_OK)
|
||||
self.win.set_icon(TRYTON_ICON)
|
||||
self.win.set_has_separator(True)
|
||||
self.win.vbox.set_spacing(3)
|
||||
self.win.vbox.pack_start(gtk.Label(
|
||||
_('Select a revision')), expand=False, fill=True)
|
||||
self.win.vbox.pack_start(gtk.HSeparator())
|
||||
hbox = gtk.HBox(spacing=3)
|
||||
hbox.pack_start(gtk.Label(_('Revision:')), expand=True, fill=True)
|
||||
format_ = date_format() + ' %H:%M:%S.%f'
|
||||
self.entry = DateEntry(format_)
|
||||
if revision:
|
||||
self.entry.set_text(datetime_strftime(revision, format_))
|
||||
list_store = gtk.ListStore(str, str)
|
||||
list_store.append(('', ''))
|
||||
for revision, id_, name in revisions:
|
||||
list_store.append((datetime_strftime(revision, format_), name))
|
||||
combobox = gtk.ComboBoxEntry(list_store)
|
||||
combobox.add(self.entry)
|
||||
cell = gtk.CellRendererText()
|
||||
combobox.pack_start(cell, True)
|
||||
combobox.add_attribute(cell, 'text', 1)
|
||||
hbox.pack_start(combobox, expand=True, fill=True)
|
||||
combobox.set_entry_text_column(0)
|
||||
combobox.connect('changed', self.changed)
|
||||
self.entry.set_property('activates_default', True)
|
||||
self.win.vbox.pack_start(hbox, expand=True, fill=True)
|
||||
self.win.show_all()
|
||||
|
||||
def changed(self, combobox):
|
||||
# set_text must be called because DateEntry doesn't work correctly with
|
||||
# the default insert/delete text behavior of combobox
|
||||
model = combobox.get_model()
|
||||
idx = combobox.get_active()
|
||||
if idx >= 0:
|
||||
self.entry.set_text(model[idx][0])
|
||||
|
||||
def run(self):
|
||||
response = self.win.run()
|
||||
revision = None
|
||||
if response == gtk.RESPONSE_OK:
|
||||
revision = self.entry.date_get()
|
||||
self.parent.present()
|
||||
self.win.destroy()
|
||||
return revision
|
||||
@@ -43,7 +43,7 @@ class TabContent(object):
|
||||
self.widget.pack_start(self.scrolledwindow)
|
||||
|
||||
def make_title_bar(self):
|
||||
title = gtk.Label()
|
||||
self.title = title = gtk.Label()
|
||||
title.modify_font(pango.FontDescription("bold 14"))
|
||||
title.set_label(self.name)
|
||||
title.set_padding(10, 4)
|
||||
|
||||
@@ -602,7 +602,7 @@ class Record(SignalEvent):
|
||||
'search_count', [
|
||||
('resource', '=',
|
||||
'%s,%s' % (self.model_name, self.id)),
|
||||
])
|
||||
], context=self.context_get())
|
||||
except RPCException:
|
||||
return 0
|
||||
return self.attachment_count
|
||||
|
||||
@@ -46,8 +46,6 @@ class Screen(SignalEvent):
|
||||
view_ids = []
|
||||
if mode is None:
|
||||
mode = ['tree', 'form']
|
||||
if context is None:
|
||||
context = {}
|
||||
if views_preload is None:
|
||||
views_preload = {}
|
||||
if domain is None:
|
||||
@@ -69,7 +67,6 @@ class Screen(SignalEvent):
|
||||
self.size_limit = None
|
||||
self.views_preload = views_preload
|
||||
self.model_name = model_name
|
||||
self.context = context
|
||||
self.views = []
|
||||
self.view_ids = view_ids[:]
|
||||
self.parent = None
|
||||
@@ -80,7 +77,7 @@ class Screen(SignalEvent):
|
||||
lambda: collections.defaultdict(lambda: None))
|
||||
self.tree_states_done = set()
|
||||
self.__group = None
|
||||
self.new_group()
|
||||
self.new_group(context or {})
|
||||
self.__current_record = None
|
||||
self.current_record = None
|
||||
self.screen_container = ScreenContainer(tab_domain)
|
||||
@@ -230,6 +227,10 @@ class Screen(SignalEvent):
|
||||
self.load(ids)
|
||||
return bool(ids)
|
||||
|
||||
@property
|
||||
def context(self):
|
||||
return self.group.context
|
||||
|
||||
def __get_group(self):
|
||||
return self.__group
|
||||
|
||||
@@ -260,9 +261,10 @@ class Screen(SignalEvent):
|
||||
|
||||
group = property(__get_group, __set_group)
|
||||
|
||||
def new_group(self):
|
||||
def new_group(self, context=None):
|
||||
context = context if context is not None else self.context
|
||||
self.group = Group(self.model_name, {}, domain=self.domain,
|
||||
context=self.context, readonly=self.readonly)
|
||||
context=context, readonly=self.readonly)
|
||||
|
||||
def _group_cleared(self, group, signal):
|
||||
for view in self.views:
|
||||
|
||||
@@ -122,6 +122,7 @@ class WinForm(NoModal):
|
||||
hbox = gtk.HBox(homogeneous=False, spacing=0)
|
||||
tooltips = common.Tooltips()
|
||||
access = common.MODELACCESS[screen.model_name]
|
||||
readonly = self.screen.group.readonly
|
||||
|
||||
if domain is not None:
|
||||
self.wid_text = gtk.Entry()
|
||||
@@ -140,7 +141,7 @@ class WinForm(NoModal):
|
||||
self.but_add.add(img_add)
|
||||
self.but_add.set_relief(gtk.RELIEF_NONE)
|
||||
hbox.pack_start(self.but_add, expand=False, fill=False)
|
||||
if not access['read']:
|
||||
if not access['read'] or readonly:
|
||||
self.but_add.set_sensitive(False)
|
||||
|
||||
self.but_remove = gtk.Button()
|
||||
@@ -153,7 +154,7 @@ class WinForm(NoModal):
|
||||
self.but_remove.add(img_remove)
|
||||
self.but_remove.set_relief(gtk.RELIEF_NONE)
|
||||
hbox.pack_start(self.but_remove, expand=False, fill=False)
|
||||
if not access['read']:
|
||||
if not access['read'] or readonly:
|
||||
self.but_remove.set_sensitive(False)
|
||||
|
||||
hbox.pack_start(gtk.VSeparator(), expand=False, fill=True)
|
||||
@@ -167,7 +168,7 @@ class WinForm(NoModal):
|
||||
self.but_new.add(img_new)
|
||||
self.but_new.set_relief(gtk.RELIEF_NONE)
|
||||
hbox.pack_start(self.but_new, expand=False, fill=False)
|
||||
if not access['create']:
|
||||
if not access['create'] or readonly:
|
||||
self.but_new.set_sensitive(False)
|
||||
|
||||
self.but_del = gtk.Button()
|
||||
@@ -180,7 +181,7 @@ class WinForm(NoModal):
|
||||
self.but_del.add(img_del)
|
||||
self.but_del.set_relief(gtk.RELIEF_NONE)
|
||||
hbox.pack_start(self.but_del, expand=False, fill=False)
|
||||
if not access['delete']:
|
||||
if not access['delete'] or readonly:
|
||||
self.but_del.set_sensitive(False)
|
||||
|
||||
self.but_undel = gtk.Button()
|
||||
@@ -194,6 +195,8 @@ class WinForm(NoModal):
|
||||
self.but_undel.add(img_undel)
|
||||
self.but_undel.set_relief(gtk.RELIEF_NONE)
|
||||
hbox.pack_start(self.but_undel, expand=False, fill=False)
|
||||
if not access['delete'] or readonly:
|
||||
self.but_undel.set_sensitive(False)
|
||||
|
||||
hbox.pack_start(gtk.VSeparator(), expand=False, fill=True)
|
||||
|
||||
@@ -355,9 +358,10 @@ class WinForm(NoModal):
|
||||
|
||||
def _sig_label(self, screen, signal_data):
|
||||
name = '_'
|
||||
access = common.MODELACCESS[screen.model_name]
|
||||
readonly = screen.group.readonly
|
||||
if signal_data[0] >= 1:
|
||||
name = str(signal_data[0])
|
||||
self.but_del.set_sensitive(True)
|
||||
if self.domain is not None:
|
||||
self.but_remove.set_sensitive(True)
|
||||
if signal_data[0] < signal_data[1]:
|
||||
@@ -368,8 +372,9 @@ class WinForm(NoModal):
|
||||
self.but_pre.set_sensitive(True)
|
||||
else:
|
||||
self.but_pre.set_sensitive(False)
|
||||
self.but_del.set_sensitive(True)
|
||||
self.but_undel.set_sensitive(True)
|
||||
if access['delete'] and not readonly:
|
||||
self.but_del.set_sensitive(True)
|
||||
self.but_undel.set_sensitive(True)
|
||||
else:
|
||||
self.but_del.set_sensitive(False)
|
||||
self.but_undel.set_sensitive(False)
|
||||
|
||||
Reference in new issue
Block a user