[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.descriptors import ClassDescriptor
from appy.gen.mail import sendMail from appy.gen.mail import sendMail
from appy.shared import mimeTypes 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 from appy.shared.data import languages
try: try:
from AccessControl.ZopeSecurityPolicy import _noroles from AccessControl.ZopeSecurityPolicy import _noroles
@ -1060,10 +1060,11 @@ class ToolMixin(BaseMixin):
url = appyUser.o.getUrl(mode='edit', page='main', nav='') url = appyUser.o.getUrl(mode='edit', page='main', nav='')
return (' | '.join(info), url) 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 '''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 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() tool = self.appy()
if not login: login = tool.user.getId() if not login: login = tool.user.getId()
user = tool.search1('User', noSecurity=True, login=login) user = tool.search1('User', noSecurity=True, login=login)
@ -1071,8 +1072,11 @@ class ToolMixin(BaseMixin):
firstName = user.firstName firstName = user.firstName
name = user.name name = user.name
res = '' res = ''
if firstName: res += firstName if firstName:
if normalized: firstName = normalizeString(firstName)
res += firstName
if name: if name:
if normalized: name = normalizeString(name)
if res: res += ' ' + name if res: res += ' ' + name
else: res = name else: res = name
if not res: res = login 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.utils import sequenceTypes,normalizeText,Traceback,getMimeType
from appy.shared.data import rtlLanguages from appy.shared.data import rtlLanguages
from appy.shared.xml_parser import XmlMarshaller from appy.shared.xml_parser import XmlMarshaller
from appy.shared.diff import HtmlDiff
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
class BaseMixin: class BaseMixin:
@ -999,17 +1000,89 @@ class BaseMixin:
return True return True
return False 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, 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
recent change first) if p_reverse is True.''' 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] key = self.workflow_history.keys()[0]
history = list(self.workflow_history[key][1:]) history = list(self.workflow_history[key][1:])
if not includeInvisible: if not includeInvisible:
history = [e for e in history if e['comments'] != '_invisible_'] history = [e for e in history if e['comments'] != '_invisible_']
if reverse: history.reverse() if reverse: history.reverse()
return {'events': history[startNumber:startNumber+batchSize], # Keep only events which are within the batch.
'totalNumber': len(history)} 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): def mayNavigate(self):
'''May the currently logged user see the navigation panel linked to '''May the currently logged user see the navigation panel linked to

View file

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

View file

@ -196,6 +196,8 @@ appyLabels = [
('event_span', 'Extend the event on the following number of days (leave ' \ ('event_span', 'Extend the event on the following number of days (leave ' \
'blank to create an event on the current day only):'), 'blank to create an event on the current day only):'),
('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_delete', 'Deleted by ${userName}'),
] ]
# 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

@ -44,7 +44,8 @@ img { border: 0; vertical-align: middle}
/* Styles that apply when viewing content of XHTML fields, that mimic styles /* Styles that apply when viewing content of XHTML fields, that mimic styles
that ckeditor uses for displaying XHTML content in the edit view. */ 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 img { margin-right: 5px }
.xhtml p { margin: 3px 0 7px 0} .xhtml p { margin: 3px 0 7px 0}

View file

@ -119,14 +119,8 @@
</tr> </tr>
<tr tal:repeat="change event/changes/items" valign="top"> <tr tal:repeat="change event/changes/items" valign="top">
<tal:change define="appyType python:contextObj.getAppyType(change[0], asDict=True);"> <tal:change define="appyType python:contextObj.getAppyType(change[0], asDict=True);">
<td tal:content="structure python: _(appyType['labelId'])"></td> <td tal:content="structure python: _(appyType['labelId'])"></td>
<td tal:define="appyValue python: contextObj.getFormattedFieldValue(change[0], change[1][0]); <td tal:content="structure python:change[1][0]"></td>
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>
</tal:change> </tal:change>
</tr> </tr>
</table> </table>

View file

@ -204,7 +204,8 @@
<tal:other condition="python: widget['name'] != 'title'"> <tal:other condition="python: widget['name'] != 'title'">
<tal:field define="contextObj python:obj; <tal:field define="contextObj python:obj;
layoutType python: 'cell'; 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" /> <metal:field use-macro="app/ui/widgets/show/macros/field" />
</tal:field> </tal:field>
</tal:other> </tal:other>

View file

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