[gen] Allow to display, in history, differences between successive versions of XHTML fields via appy.shared.diff.HtmlDiff (which was not integrated to appy.gen until now). Improved rendering of XHTML fields. [pod] bugfix while generating filenames for files included into pod results.

This commit is contained in:
Gaetan Delannay 2013-01-07 15:30:13 +01:00
parent 6c832e43bb
commit 240ce59519
8 changed files with 94 additions and 20 deletions

View file

@ -9,7 +9,7 @@ from appy.gen.wrappers import AbstractWrapper
from appy.gen.descriptors import ClassDescriptor
from appy.gen.mail import sendMail
from appy.shared import mimeTypes
from appy.shared.utils import getOsTempFolder, sequenceTypes
from appy.shared.utils import getOsTempFolder, sequenceTypes, normalizeString
from appy.shared.data import languages
try:
from AccessControl.ZopeSecurityPolicy import _noroles
@ -1060,10 +1060,11 @@ class ToolMixin(BaseMixin):
url = appyUser.o.getUrl(mode='edit', page='main', nav='')
return (' | '.join(info), url)
def getUserName(self, login=None):
def getUserName(self, login=None, normalized=False):
'''Gets the user name corresponding to p_login (or the currently logged
login if None), or the p_login itself if the user does not exist
anymore.'''
anymore. If p_normalized is True, special chars in the first and last
names are normalized.'''
tool = self.appy()
if not login: login = tool.user.getId()
user = tool.search1('User', noSecurity=True, login=login)
@ -1071,8 +1072,11 @@ class ToolMixin(BaseMixin):
firstName = user.firstName
name = user.name
res = ''
if firstName: res += firstName
if firstName:
if normalized: firstName = normalizeString(firstName)
res += firstName
if name:
if normalized: name = normalizeString(name)
if res: res += ' ' + name
else: res = name
if not res: res = login

View file

@ -12,6 +12,7 @@ from appy.gen.descriptors import WorkflowDescriptor, ClassDescriptor
from appy.shared.utils import sequenceTypes,normalizeText,Traceback,getMimeType
from appy.shared.data import rtlLanguages
from appy.shared.xml_parser import XmlMarshaller
from appy.shared.diff import HtmlDiff
# ------------------------------------------------------------------------------
class BaseMixin:
@ -999,17 +1000,89 @@ class BaseMixin:
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
it does not find it there, it returns the current value on p_obj.'''
i = stopIndex + 1
while (i-1) >= 0:
i -= 1
if history[i]['action'] != '_datachange_': continue
if field.name not in history[i]['changes']: continue
# We have found it!
return history[i]['changes'][field.name][0] or ''
return field.getValue(self) or ''
def getHistoryTexts(self, event):
'''Returns a tuple (insertText, deleteText) containing texts to show on,
respectively, inserted and deleted chunks of text in a XHTML diff.'''
tool = self.getTool()
userName = tool.getUserName(event['actor'])
mapping = {'userName': userName.decode('utf-8')}
res = []
for type in ('insert', 'delete'):
msg = self.translate('history_%s' % type, mapping=mapping)
date = tool.formatDate(event['time'], withHour=True)
msg = '%s: %s' % (date, msg)
res.append(msg.encode('utf-8'))
return res
def getHistory(self, startNumber=0, reverse=True, includeInvisible=False,
batchSize=5):
'''Returns the history for this object, sorted in reverse order (most
recent change first) if p_reverse is True.'''
# Get a copy of the history, reversed if needed, whose invisible events
# have been removed if needed.
key = self.workflow_history.keys()[0]
history = list(self.workflow_history[key][1:])
if not includeInvisible:
history = [e for e in history if e['comments'] != '_invisible_']
if reverse: history.reverse()
return {'events': history[startNumber:startNumber+batchSize],
'totalNumber': len(history)}
# Keep only events which are within the batch.
res = []
stopIndex = startNumber + batchSize - 1
i = -1
while (i+1) < len(history):
i += 1
# Ignore events outside range startNumber:startNumber+batchSize
if i < startNumber: continue
if i > stopIndex: break
if history[i]['action'] == '_datachange_':
# Take a copy of the event: we will modify it and replace
# fields' old values by their formatted counterparts.
event = history[i].copy()
event['changes'] = {}
for name, oldValue in history[i]['changes'].iteritems():
# oldValue is a tuple (value, fieldName).
field = self.getAppyType(name)
# Field 'name' may not exist, if the history has been
# transferred from another site. In this case we can't show
# this data change.
if not field: continue
if (field.type == 'String') and \
(field.format == gen.String.XHTML):
# For rich text fields, instead of simply showing the
# previous value, we propose a diff with the next
# version.
if field.isEmptyValue(oldValue[0]):
val = '-'
else:
newValue = self.findNewValue(field, history, i-1)
# Compute the diff between oldValue and newValue
iMsg, dMsg = self.getHistoryTexts(event)
comparator= HtmlDiff(oldValue[0],newValue,iMsg,dMsg)
val = comparator.get()
event['changes'][name] = (val, oldValue[1])
else:
val = field.getFormattedValue(self, oldValue[0]) or '-'
if isinstance(val, list) or isinstance(val, tuple):
val = '<ul>%s</ul>' % \
''.join(['<li>%s</li>' % v for v in val])
event['changes'][name] = (val, oldValue[1])
else:
event = history[i]
res.append(event)
return {'events': res, 'totalNumber': len(history)}
def mayNavigate(self):
'''May the currently logged user see the navigation panel linked to

View file

@ -2,7 +2,6 @@
import sha
from appy import Object
from appy.gen import Type
from appy.shared.utils import normalizeString
# ------------------------------------------------------------------------------
class OgoneConfig:
@ -96,7 +95,7 @@ class Ogone(Type):
del res['shaOutKey']
res.update(self.callMethod(obj, self.orderMethod))
# Add user-related information
res['CN'] = str(normalizeString(tool.getUserName()))
res['CN'] = str(tool.getUserName(normalized=True))
user = obj.appy().appyUser
res['EMAIL'] = user.email or user.login
# Add standard back URLs

View file

@ -196,6 +196,8 @@ appyLabels = [
('event_span', 'Extend the event on the following number of days (leave ' \
'blank to create an event on the current day only):'),
('del_next_events', 'Also delete successive events of the same type.'),
('history_insert', 'Inserted by ${userName}'),
('history_delete', 'Deleted by ${userName}'),
]
# Some default values for labels whose ids are not fixed (so they can't be

View file

@ -44,7 +44,8 @@ img { border: 0; vertical-align: middle}
/* Styles that apply when viewing content of XHTML fields, that mimic styles
that ckeditor uses for displaying XHTML content in the edit view. */
.xhtml { margin-top: 5px }
.xhtml { margin-top: 5px; background-color: white;
padding: 6px; border: 1px dashed grey; border-radius: 0.3em }
.xhtml img { margin-right: 5px }
.xhtml p { margin: 3px 0 7px 0}

View file

@ -120,13 +120,7 @@
<tr tal:repeat="change event/changes/items" valign="top">
<tal:change define="appyType python:contextObj.getAppyType(change[0], asDict=True);">
<td tal:content="structure python: _(appyType['labelId'])"></td>
<td tal:define="appyValue python: contextObj.getFormattedFieldValue(change[0], change[1][0]);
severalValues python: (appyType['multiplicity'][1] &gt; 1) or (appyType['multiplicity'][1] == None)">
<span tal:condition="not: severalValues" tal:replace="appyValue"></span>
<ul tal:condition="python: severalValues">
<li tal:repeat="av appyValue" tal:content="av"></li>
</ul>
</td>
<td tal:content="structure python:change[1][0]"></td>
</tal:change>
</tr>
</table>

View file

@ -204,7 +204,8 @@
<tal:other condition="python: widget['name'] != 'title'">
<tal:field define="contextObj python:obj;
layoutType python: 'cell';
innerRef python:True">
innerRef python:True"
condition="python: obj.showField(widget['name'], layoutType='result')">
<metal:field use-macro="app/ui/widgets/show/macros/field" />
</tal:field>
</tal:other>

View file

@ -95,7 +95,7 @@ class DocImporter:
format = '' # We will know it only after the HTTP GET.
else:
format = os.path.splitext(at)[1][1:]
fileName = 'f.%d.%f.%s' % (random.randint(0,10), time.time(), format)
fileName = 'f.%d.%f.%s' % (random.randint(0,1000), time.time(), format)
return os.path.abspath('%s/%s' % (self.importFolder, fileName))
def moveFile(self, at, importPath):
@ -288,7 +288,7 @@ class ImageImporter(DocImporter):
t = self.textNs
x = self.linkNs
s = self.svgNs
imageName = 'Image%f' % time.time()
imageName = 'Image%f.%d' % (time.time(), random.randint(0,1000))
# Compute path to image
i = self.importPath.rfind(self.pictFolder)
imagePath = self.importPath[i+1:].replace('\\', '/')