[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.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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
|
||||
|
|
|
@ -119,14 +119,8 @@
|
|||
</tr>
|
||||
<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] > 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: _(appyType['labelId'])"></td>
|
||||
<td tal:content="structure python:change[1][0]"></td>
|
||||
</tal:change>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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('\\', '/')
|
||||
|
|
Loading…
Reference in a new issue