[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:
parent
6c832e43bb
commit
240ce59519
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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}
|
||||||
|
|
||||||
|
|
|
@ -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] > 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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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('\\', '/')
|
||||||
|
|
Loading…
Reference in a new issue