[gen] Allow to show the cumulative differences performed on historized String fields with format=String.XHTML.
This commit is contained in:
parent
6caeeb1761
commit
ab00917df6
|
@ -12,6 +12,7 @@ import appy.pod
|
||||||
from appy.pod.renderer import Renderer
|
from appy.pod.renderer import Renderer
|
||||||
from appy.shared.data import countries
|
from appy.shared.data import countries
|
||||||
from appy.shared.xml_parser import XhtmlCleaner
|
from appy.shared.xml_parser import XhtmlCleaner
|
||||||
|
from appy.shared.diff import HtmlDiff
|
||||||
from appy.shared.utils import Traceback, getOsTempFolder, formatNumber, \
|
from appy.shared.utils import Traceback, getOsTempFolder, formatNumber, \
|
||||||
FileWrapper, sequenceTypes
|
FileWrapper, sequenceTypes
|
||||||
|
|
||||||
|
@ -678,8 +679,9 @@ class Type:
|
||||||
layouts['view'].addCssClasses('focus')
|
layouts['view'].addCssClasses('focus')
|
||||||
layouts['edit'].addCssClasses('focus')
|
layouts['edit'].addCssClasses('focus')
|
||||||
# If layouts are the default ones, set width=None instead of width=100%
|
# If layouts are the default ones, set width=None instead of width=100%
|
||||||
# for the field if it is not in a group.
|
# for the field if it is not in a group (excepted for rich texts).
|
||||||
if areDefault and not self.group:
|
if areDefault and not self.group and \
|
||||||
|
not ((self.type == 'String') and (self.format == String.XHTML)):
|
||||||
for layoutType in layouts.iterkeys():
|
for layoutType in layouts.iterkeys():
|
||||||
layouts[layoutType].width = ''
|
layouts[layoutType].width = ''
|
||||||
# Remove letters "r" from the layouts if the field is not required.
|
# Remove letters "r" from the layouts if the field is not required.
|
||||||
|
@ -765,10 +767,12 @@ class Type:
|
||||||
return self.default
|
return self.default
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def getFormattedValue(self, obj, value):
|
def getFormattedValue(self, obj, value, showChanges=False):
|
||||||
'''p_value is a real p_obj(ect) value from a field from this type. This
|
'''p_value is a real p_obj(ect) value from a field from this type. This
|
||||||
method returns a pretty, string-formatted version, for displaying
|
method returns a pretty, string-formatted version, for displaying
|
||||||
purposes. Needs to be overridden by some child classes.'''
|
purposes. Needs to be overridden by some child classes. If
|
||||||
|
p_showChanges is True, the result must also include the changes that
|
||||||
|
occurred on p_value across the ages.'''
|
||||||
if self.isEmptyValue(value): return ''
|
if self.isEmptyValue(value): return ''
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
@ -971,7 +975,7 @@ class Integer(Type):
|
||||||
def getStorableValue(self, value):
|
def getStorableValue(self, value):
|
||||||
if not self.isEmptyValue(value): return self.pythonType(value)
|
if not self.isEmptyValue(value): return self.pythonType(value)
|
||||||
|
|
||||||
def getFormattedValue(self, obj, value):
|
def getFormattedValue(self, obj, value, showChanges=False):
|
||||||
if self.isEmptyValue(value): return ''
|
if self.isEmptyValue(value): return ''
|
||||||
return str(value)
|
return str(value)
|
||||||
|
|
||||||
|
@ -1009,7 +1013,7 @@ class Float(Type):
|
||||||
label, sdefault, scolspan)
|
label, sdefault, scolspan)
|
||||||
self.pythonType = float
|
self.pythonType = float
|
||||||
|
|
||||||
def getFormattedValue(self, obj, value):
|
def getFormattedValue(self, obj, value, showChanges=False):
|
||||||
return formatNumber(value, sep=self.sep[0], precision=self.precision,
|
return formatNumber(value, sep=self.sep[0], precision=self.precision,
|
||||||
tsep=self.tsep)
|
tsep=self.tsep)
|
||||||
|
|
||||||
|
@ -1214,7 +1218,7 @@ class String(Type):
|
||||||
view = 'lc-f'
|
view = 'lc-f'
|
||||||
else:
|
else:
|
||||||
view = 'l-f'
|
view = 'l-f'
|
||||||
return {'view': view, 'edit': 'lrv-d-f'}
|
return {'view': Table(view, width='100%'), 'edit': 'lrv-d-f'}
|
||||||
elif self.isMultiValued():
|
elif self.isMultiValued():
|
||||||
return {'view': 'l-f', 'edit': 'lrv-f'}
|
return {'view': 'l-f', 'edit': 'lrv-f'}
|
||||||
|
|
||||||
|
@ -1246,7 +1250,31 @@ class String(Type):
|
||||||
type='warning')
|
type='warning')
|
||||||
Type.store(self, obj, value)
|
Type.store(self, obj, value)
|
||||||
|
|
||||||
def getFormattedValue(self, obj, value):
|
def getDiffValue(self, obj, value):
|
||||||
|
'''Returns a version of p_value that includes the cumulative diffs
|
||||||
|
between successive versions.'''
|
||||||
|
res = None
|
||||||
|
lastEvent = None
|
||||||
|
for event in obj.workflow_history.values()[0]:
|
||||||
|
if event['action'] != '_datachange_': continue
|
||||||
|
if self.name not in event['changes']: continue
|
||||||
|
if res == None:
|
||||||
|
# We have found the first version of the field
|
||||||
|
res = event['changes'][self.name][0] or ''
|
||||||
|
else:
|
||||||
|
# We need to produce the difference between current result and
|
||||||
|
# this version.
|
||||||
|
iMsg, dMsg = obj.getHistoryTexts(lastEvent)
|
||||||
|
thisVersion = event['changes'][self.name][0] or ''
|
||||||
|
comparator = HtmlDiff(res, thisVersion, iMsg, dMsg)
|
||||||
|
res = comparator.get()
|
||||||
|
lastEvent = event
|
||||||
|
# Now we need to compare the result with the current version.
|
||||||
|
iMsg, dMsg = obj.getHistoryTexts(lastEvent)
|
||||||
|
comparator = HtmlDiff(res, value or '', iMsg, dMsg)
|
||||||
|
return comparator.get()
|
||||||
|
|
||||||
|
def getFormattedValue(self, obj, value, showChanges=False):
|
||||||
if self.isEmptyValue(value): return ''
|
if self.isEmptyValue(value): return ''
|
||||||
res = value
|
res = value
|
||||||
if self.isSelect:
|
if self.isSelect:
|
||||||
|
@ -1265,6 +1293,9 @@ class String(Type):
|
||||||
res = [t('%s_list_%s' % (self.labelId, v)) for v in value]
|
res = [t('%s_list_%s' % (self.labelId, v)) for v in value]
|
||||||
else:
|
else:
|
||||||
res = t('%s_list_%s' % (self.labelId, value))
|
res = t('%s_list_%s' % (self.labelId, value))
|
||||||
|
elif (self.format == String.XHTML) and showChanges:
|
||||||
|
# Compute the successive changes that occurred on p_value.
|
||||||
|
res = self.getDiffValue(obj, res)
|
||||||
# If value starts with a carriage return, add a space; else, it will
|
# If value starts with a carriage return, add a space; else, it will
|
||||||
# be ignored.
|
# be ignored.
|
||||||
if isinstance(res, basestring) and \
|
if isinstance(res, basestring) and \
|
||||||
|
@ -1477,7 +1508,7 @@ class Boolean(Type):
|
||||||
if value == None: return False
|
if value == None: return False
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def getFormattedValue(self, obj, value):
|
def getFormattedValue(self, obj, value, showChanges=False):
|
||||||
if value: res = obj.translate('yes')
|
if value: res = obj.translate('yes')
|
||||||
else: res = obj.translate('no')
|
else: res = obj.translate('no')
|
||||||
return res
|
return res
|
||||||
|
@ -1542,7 +1573,7 @@ class Date(Type):
|
||||||
except DateTime.DateError, ValueError:
|
except DateTime.DateError, ValueError:
|
||||||
return obj.translate('bad_date')
|
return obj.translate('bad_date')
|
||||||
|
|
||||||
def getFormattedValue(self, obj, value):
|
def getFormattedValue(self, obj, value, showChanges=False):
|
||||||
if self.isEmptyValue(value): return ''
|
if self.isEmptyValue(value): return ''
|
||||||
tool = obj.getTool().appy()
|
tool = obj.getTool().appy()
|
||||||
# A problem may occur with some extreme year values. Replace the "year"
|
# A problem may occur with some extreme year values. Replace the "year"
|
||||||
|
@ -1621,7 +1652,7 @@ class File(Type):
|
||||||
if value: value = FileWrapper(value)
|
if value: value = FileWrapper(value)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def getFormattedValue(self, obj, value):
|
def getFormattedValue(self, obj, value, showChanges=False):
|
||||||
if not value: return value
|
if not value: return value
|
||||||
return value._zopeFile
|
return value._zopeFile
|
||||||
|
|
||||||
|
@ -1901,7 +1932,7 @@ class Ref(Type):
|
||||||
if someObjects: return res
|
if someObjects: return res
|
||||||
return res.objects
|
return res.objects
|
||||||
|
|
||||||
def getFormattedValue(self, obj, value):
|
def getFormattedValue(self, obj, value, showChanges=False):
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def getIndexType(self): return 'ListIndex'
|
def getIndexType(self): return 'ListIndex'
|
||||||
|
@ -2142,7 +2173,7 @@ class Computed(Type):
|
||||||
# self.method is a method that will return the field value
|
# self.method is a method that will return the field value
|
||||||
return self.callMethod(obj, self.method, raiseOnError=True)
|
return self.callMethod(obj, self.method, raiseOnError=True)
|
||||||
|
|
||||||
def getFormattedValue(self, obj, value):
|
def getFormattedValue(self, obj, value, showChanges=False):
|
||||||
if not isinstance(value, basestring): return str(value)
|
if not isinstance(value, basestring): return str(value)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
|
@ -514,10 +514,10 @@ class BaseMixin:
|
||||||
listType = self.getAppyType(listName)
|
listType = self.getAppyType(listName)
|
||||||
return listType.getInnerValue(outerValue, name, int(i))
|
return listType.getInnerValue(outerValue, name, int(i))
|
||||||
|
|
||||||
def getFormattedFieldValue(self, name, value):
|
def getFormattedFieldValue(self, name, value, showChanges):
|
||||||
'''Gets a nice, string representation of p_value which is a value from
|
'''Gets a nice, string representation of p_value which is a value from
|
||||||
field named p_name.'''
|
field named p_name.'''
|
||||||
return self.getAppyType(name).getFormattedValue(self, value)
|
return self.getAppyType(name).getFormattedValue(self,value,showChanges)
|
||||||
|
|
||||||
def getRequestFieldValue(self, name):
|
def getRequestFieldValue(self, name):
|
||||||
'''Gets the value of field p_name as may be present in the request.'''
|
'''Gets the value of field p_name as may be present in the request.'''
|
||||||
|
@ -991,15 +991,6 @@ class BaseMixin:
|
||||||
return 1
|
return 1
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def hasHistory(self):
|
|
||||||
'''Has this object an history?'''
|
|
||||||
if hasattr(self.aq_base, 'workflow_history') and self.workflow_history:
|
|
||||||
key = self.workflow_history.keys()[0]
|
|
||||||
for event in self.workflow_history[key]:
|
|
||||||
if event['action'] and (event['comments'] != '_invisible_'):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def findNewValue(self, field, history, stopIndex):
|
def findNewValue(self, field, history, stopIndex):
|
||||||
'''This function tries to find a more recent version of value of p_field
|
'''This function tries to find a more recent version of value of p_field
|
||||||
on p_self. It first tries to find it in history[:stopIndex+1]. If
|
on p_self. It first tries to find it in history[:stopIndex+1]. If
|
||||||
|
@ -1027,6 +1018,20 @@ class BaseMixin:
|
||||||
res.append(msg.encode('utf-8'))
|
res.append(msg.encode('utf-8'))
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
def hasHistory(self, fieldName=None):
|
||||||
|
'''Has this object an history? If p_fieldName is specified, the question
|
||||||
|
becomes: has this object an history for field p_fieldName?'''
|
||||||
|
if hasattr(self.aq_base, 'workflow_history') and self.workflow_history:
|
||||||
|
history = self.workflow_history.values()[0]
|
||||||
|
if not fieldName:
|
||||||
|
for event in history:
|
||||||
|
if event['action'] and (event['comments'] != '_invisible_'):
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
for event in history:
|
||||||
|
if (event['action'] == '_datachange_') and \
|
||||||
|
(fieldName in event['changes']): return True
|
||||||
|
|
||||||
def getHistory(self, startNumber=0, reverse=True, includeInvisible=False,
|
def getHistory(self, startNumber=0, reverse=True, includeInvisible=False,
|
||||||
batchSize=5):
|
batchSize=5):
|
||||||
'''Returns the history for this object, sorted in reverse order (most
|
'''Returns the history for this object, sorted in reverse order (most
|
||||||
|
|
|
@ -198,6 +198,8 @@ appyLabels = [
|
||||||
('del_next_events', 'Also delete successive events of the same type.'),
|
('del_next_events', 'Also delete successive events of the same type.'),
|
||||||
('history_insert', 'Inserted by ${userName}'),
|
('history_insert', 'Inserted by ${userName}'),
|
||||||
('history_delete', 'Deleted by ${userName}'),
|
('history_delete', 'Deleted by ${userName}'),
|
||||||
|
('changes_show', 'Show changes'),
|
||||||
|
('changes_hide', 'Hide changes'),
|
||||||
]
|
]
|
||||||
|
|
||||||
# Some default values for labels whose ids are not fixed (so they can't be
|
# Some default values for labels whose ids are not fixed (so they can't be
|
||||||
|
|
|
@ -192,6 +192,14 @@ function askComputedField(hookId, objectUrl, fieldName) {
|
||||||
askAjaxChunk(hookId, 'GET', objectUrl, 'widgets/computed', 'viewContent', params);
|
askAjaxChunk(hookId, 'GET', objectUrl, 'widgets/computed', 'viewContent', params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function askField(hookId, objectUrl, layoutType, showChanges){
|
||||||
|
// Sends an Ajax request for getting the content of any field.
|
||||||
|
var fieldName = hookId.split('_')[1];
|
||||||
|
var params = {'fieldName': fieldName, 'layoutType': layoutType,
|
||||||
|
'showChanges': showChanges};
|
||||||
|
askAjaxChunk(hookId, 'GET', objectUrl, 'widgets/show', 'fieldAjax', params);
|
||||||
|
}
|
||||||
|
|
||||||
// Function used by checkbox widgets for having radio-button-like behaviour
|
// Function used by checkbox widgets for having radio-button-like behaviour
|
||||||
function toggleCheckbox(visibleCheckbox, hiddenBoolean) {
|
function toggleCheckbox(visibleCheckbox, hiddenBoolean) {
|
||||||
vis = document.getElementById(visibleCheckbox);
|
vis = document.getElementById(visibleCheckbox);
|
||||||
|
|
|
@ -73,7 +73,7 @@
|
||||||
<metal:history define-macro="objectHistory"
|
<metal:history define-macro="objectHistory"
|
||||||
tal:define="startNumber request/startNumber|python:0;
|
tal:define="startNumber request/startNumber|python:0;
|
||||||
startNumber python: int(startNumber);
|
startNumber python: int(startNumber);
|
||||||
batchSize python: int(request.get('maxPerPage'));
|
batchSize python: int(request.get('maxPerPage', 5));
|
||||||
historyInfo python: contextObj.getHistory(startNumber, batchSize=batchSize);
|
historyInfo python: contextObj.getHistory(startNumber, batchSize=batchSize);
|
||||||
objs historyInfo/events;
|
objs historyInfo/events;
|
||||||
totalNumber historyInfo/totalNumber;
|
totalNumber historyInfo/totalNumber;
|
||||||
|
|
|
@ -5,11 +5,12 @@
|
||||||
layoutType The kind of layout: "view"? "edit"? "cell"?
|
layoutType The kind of layout: "view"? "edit"? "cell"?
|
||||||
layout The layout object that will dictate how object content
|
layout The layout object that will dictate how object content
|
||||||
will be rendered.
|
will be rendered.
|
||||||
|
tagId The ID of the main tag for this layout.
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
contextMacro The base folder containing the macros to call for
|
contextMacro The base folder containing the macros to call for
|
||||||
rendering the elements within the layout.
|
rendering the elements within the layout.
|
||||||
Defaults to app.ui
|
Defaults to app.ui
|
||||||
tagId The ID of the main tag for this layout.
|
|
||||||
tagName The name of the main tag for this layout (used
|
tagName The name of the main tag for this layout (used
|
||||||
a.o. for master/slave relationships).
|
a.o. for master/slave relationships).
|
||||||
tagCss Some additional CSS class for the main tag
|
tagCss Some additional CSS class for the main tag
|
||||||
|
@ -59,15 +60,19 @@
|
||||||
Optionally:
|
Optionally:
|
||||||
widgetName If the field to show is within a List, we cheat and
|
widgetName If the field to show is within a List, we cheat and
|
||||||
include, within the widgetName, the row index.
|
include, within the widgetName, the row index.
|
||||||
|
showChanges Do we need to show a variant of the field showing
|
||||||
|
successive changes made to it?
|
||||||
|
|
||||||
</tal:comment>
|
</tal:comment>
|
||||||
<metal:field define-macro="field"
|
<metal:field define-macro="field"
|
||||||
tal:define="contextMacro python: app.ui.widgets;
|
tal:define="contextMacro python: app.ui.widgets;
|
||||||
|
showChanges showChanges| python:False;
|
||||||
layout python: widget['layouts'][layoutType];
|
layout python: widget['layouts'][layoutType];
|
||||||
name widgetName| widget/name;
|
name widgetName| widget/name;
|
||||||
sync python: widget['sync'][layoutType];
|
sync python: widget['sync'][layoutType];
|
||||||
outerValue value|nothing;
|
outerValue value|nothing;
|
||||||
rawValue python: contextObj.getFieldValue(name,onlyIfSync=True,layoutType=layoutType,outerValue=outerValue);
|
rawValue python: contextObj.getFieldValue(name,onlyIfSync=True,layoutType=layoutType,outerValue=outerValue);
|
||||||
value python: contextObj.getFormattedFieldValue(name, rawValue);
|
value python: contextObj.getFormattedFieldValue(name, rawValue, showChanges);
|
||||||
requestValue python: contextObj.getRequestFieldValue(name);
|
requestValue python: contextObj.getRequestFieldValue(name);
|
||||||
inRequest python: request.has_key(name);
|
inRequest python: request.has_key(name);
|
||||||
errors errors | python: ();
|
errors errors | python: ();
|
||||||
|
@ -78,10 +83,20 @@
|
||||||
tagCss tagCss | python:'';
|
tagCss tagCss | python:'';
|
||||||
tagCss python: ('%s %s' % (slaveCss, tagCss)).strip();
|
tagCss python: ('%s %s' % (slaveCss, tagCss)).strip();
|
||||||
tagId python: '%s_%s' % (contextObj.UID(), name);
|
tagId python: '%s_%s' % (contextObj.UID(), name);
|
||||||
tagName python: widget['master'] and 'slave' or ''">
|
tagName python: widget['master'] and 'slave' or '';">
|
||||||
<metal:layout use-macro="context/ui/widgets/show/macros/layout"/>
|
<metal:layout use-macro="context/ui/widgets/show/macros/layout"/>
|
||||||
</metal:field>
|
</metal:field>
|
||||||
|
|
||||||
|
<tal:comment replace="nothing">Call the previous macro, but from Ajax.</tal:comment>
|
||||||
|
<metal:afield define-macro="fieldAjax"
|
||||||
|
tal:define="widgetName request/fieldName;
|
||||||
|
layoutType request/layoutType;
|
||||||
|
showChanges python: request.get('showChanges', 'False') == 'True';
|
||||||
|
widget python: contextObj.getAppyType(widgetName, asDict=True);
|
||||||
|
page widget/pageName">
|
||||||
|
<metal:field use-macro="context/ui/widgets/show/macros/field"/>
|
||||||
|
</metal:afield>
|
||||||
|
|
||||||
<tal:comment replace="nothing">
|
<tal:comment replace="nothing">
|
||||||
This macro displays the widget corresponding to a group of widgets.
|
This macro displays the widget corresponding to a group of widgets.
|
||||||
It requires:
|
It requires:
|
||||||
|
@ -223,4 +238,4 @@
|
||||||
<tal:required metal:define-macro="required"><img tal:attributes="src string: $appUrl/ui/required.gif"/></tal:required>
|
<tal:required metal:define-macro="required"><img tal:attributes="src string: $appUrl/ui/required.gif"/></tal:required>
|
||||||
|
|
||||||
<tal:comment replace="nothing">Button for showing changes to the field.</tal:comment>
|
<tal:comment replace="nothing">Button for showing changes to the field.</tal:comment>
|
||||||
<tal:changes metal:define-macro="changes"><img tal:attributes="src string: $appUrl/ui/changes.png"/></tal:changes>
|
<tal:changes metal:define-macro="changes" tal:condition="python: contextObj.hasHistory(name)"><img style="cursor:pointer" tal:condition="not: showChanges" tal:attributes="src string: $appUrl/ui/changes.png; onclick python: 'askField(\'%s\',\'%s\',\'view\',\'True\')' % (tagId, contextObj.absolute_url()); title python: _('changes_show')"/><img style="cursor:pointer" tal:condition="showChanges" tal:attributes="src string: $appUrl/ui/changesNo.png; onclick python: 'askField(\'%s\',\'%s\',\'view\',\'False\')' % (tagId, contextObj.absolute_url()); title python: _('changes_hide')"/></tal:changes>
|
||||||
|
|
Loading…
Reference in a new issue