[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
					
				
					 8 changed files with 94 additions and 20 deletions
				
			
		| 
						 | 
					@ -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}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -120,13 +120,7 @@
 | 
				
			||||||
          <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…
	
	Add table
		Add a link
		
	
		Reference in a new issue