[gen] Allow to show the cumulative differences performed on historized String fields with format=String.XHTML.

This commit is contained in:
Gaetan Delannay 2013-01-08 16:58:29 +01:00
parent 6caeeb1761
commit ab00917df6
6 changed files with 90 additions and 29 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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);

View file

@ -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;

View file

@ -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>