[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.shared.data import countries
from appy.shared.xml_parser import XhtmlCleaner
from appy.shared.diff import HtmlDiff
from appy.shared.utils import Traceback, getOsTempFolder, formatNumber, \
FileWrapper, sequenceTypes
@ -678,8 +679,9 @@ class Type:
layouts['view'].addCssClasses('focus')
layouts['edit'].addCssClasses('focus')
# If layouts are the default ones, set width=None instead of width=100%
# for the field if it is not in a group.
if areDefault and not self.group:
# for the field if it is not in a group (excepted for rich texts).
if areDefault and not self.group and \
not ((self.type == 'String') and (self.format == String.XHTML)):
for layoutType in layouts.iterkeys():
layouts[layoutType].width = ''
# Remove letters "r" from the layouts if the field is not required.
@ -765,10 +767,12 @@ class Type:
return self.default
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
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 ''
return value
@ -971,7 +975,7 @@ class Integer(Type):
def getStorableValue(self, 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 ''
return str(value)
@ -1009,7 +1013,7 @@ class Float(Type):
label, sdefault, scolspan)
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,
tsep=self.tsep)
@ -1214,7 +1218,7 @@ class String(Type):
view = 'lc-f'
else:
view = 'l-f'
return {'view': view, 'edit': 'lrv-d-f'}
return {'view': Table(view, width='100%'), 'edit': 'lrv-d-f'}
elif self.isMultiValued():
return {'view': 'l-f', 'edit': 'lrv-f'}
@ -1246,7 +1250,31 @@ class String(Type):
type='warning')
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 ''
res = value
if self.isSelect:
@ -1265,6 +1293,9 @@ class String(Type):
res = [t('%s_list_%s' % (self.labelId, v)) for v in value]
else:
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
# be ignored.
if isinstance(res, basestring) and \
@ -1477,7 +1508,7 @@ class Boolean(Type):
if value == None: return False
return value
def getFormattedValue(self, obj, value):
def getFormattedValue(self, obj, value, showChanges=False):
if value: res = obj.translate('yes')
else: res = obj.translate('no')
return res
@ -1542,7 +1573,7 @@ class Date(Type):
except DateTime.DateError, ValueError:
return obj.translate('bad_date')
def getFormattedValue(self, obj, value):
def getFormattedValue(self, obj, value, showChanges=False):
if self.isEmptyValue(value): return ''
tool = obj.getTool().appy()
# A problem may occur with some extreme year values. Replace the "year"
@ -1621,7 +1652,7 @@ class File(Type):
if value: value = FileWrapper(value)
return value
def getFormattedValue(self, obj, value):
def getFormattedValue(self, obj, value, showChanges=False):
if not value: return value
return value._zopeFile
@ -1901,7 +1932,7 @@ class Ref(Type):
if someObjects: return res
return res.objects
def getFormattedValue(self, obj, value):
def getFormattedValue(self, obj, value, showChanges=False):
return value
def getIndexType(self): return 'ListIndex'
@ -2142,7 +2173,7 @@ class Computed(Type):
# self.method is a method that will return the field value
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)
return value

View file

@ -514,10 +514,10 @@ class BaseMixin:
listType = self.getAppyType(listName)
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
field named p_name.'''
return self.getAppyType(name).getFormattedValue(self, value)
return self.getAppyType(name).getFormattedValue(self,value,showChanges)
def getRequestFieldValue(self, name):
'''Gets the value of field p_name as may be present in the request.'''
@ -991,15 +991,6 @@ class BaseMixin:
return 1
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):
'''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
@ -1027,6 +1018,20 @@ class BaseMixin:
res.append(msg.encode('utf-8'))
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,
batchSize=5):
'''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.'),
('history_insert', 'Inserted 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

View file

@ -192,6 +192,14 @@ function askComputedField(hookId, objectUrl, fieldName) {
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 toggleCheckbox(visibleCheckbox, hiddenBoolean) {
vis = document.getElementById(visibleCheckbox);

View file

@ -73,7 +73,7 @@
<metal:history define-macro="objectHistory"
tal:define="startNumber request/startNumber|python:0;
startNumber python: int(startNumber);
batchSize python: int(request.get('maxPerPage'));
batchSize python: int(request.get('maxPerPage', 5));
historyInfo python: contextObj.getHistory(startNumber, batchSize=batchSize);
objs historyInfo/events;
totalNumber historyInfo/totalNumber;

View file

@ -5,11 +5,12 @@
layoutType The kind of layout: "view"? "edit"? "cell"?
layout The layout object that will dictate how object content
will be rendered.
tagId The ID of the main tag for this layout.
Options:
contextMacro The base folder containing the macros to call for
rendering the elements within the layout.
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
a.o. for master/slave relationships).
tagCss Some additional CSS class for the main tag
@ -59,15 +60,19 @@
Optionally:
widgetName If the field to show is within a List, we cheat and
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>
<metal:field define-macro="field"
tal:define="contextMacro python: app.ui.widgets;
showChanges showChanges| python:False;
layout python: widget['layouts'][layoutType];
name widgetName| widget/name;
sync python: widget['sync'][layoutType];
outerValue value|nothing;
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);
inRequest python: request.has_key(name);
errors errors | python: ();
@ -78,10 +83,20 @@
tagCss tagCss | python:'';
tagCss python: ('%s %s' % (slaveCss, tagCss)).strip();
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: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">
This macro displays the widget corresponding to a group of widgets.
It requires:
@ -223,4 +238,4 @@
<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: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>