appy.gen: generalized use of appy.Object; existence of a field value (that triggers search for a default value) is now based on Type.isEmptyValue, and not '==None'; bugfix with default values for List fields; prevent search icon to be shown for a Ref when there is no linked object; appy.pod: added class appy.pod.parts.OdtTable that allows to create a complex (ie, with a dynamic number of columns) table programmatically (to be imported with a statement 'do ... from'); appy.shared.diff: improvements in the multiple XHTML diff; appy.shared.xml_parser.XmlMarshaller: support for default namespaces and 'any' tags.
This commit is contained in:
parent
1ebcbb7b34
commit
3ab6cec7d6
|
@ -1,11 +1,12 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
import re, time, copy, sys, types, os, os.path, mimetypes, string, StringIO
|
import re, time, copy, sys, types, os, os.path, mimetypes, string, StringIO
|
||||||
|
from appy import Object
|
||||||
from appy.gen.layout import Table
|
from appy.gen.layout import Table
|
||||||
from appy.gen.layout import defaultFieldLayouts
|
from appy.gen.layout import defaultFieldLayouts
|
||||||
from appy.gen.po import PoMessage
|
from appy.gen.po import PoMessage
|
||||||
from appy.gen.utils import sequenceTypes, GroupDescr, Keywords, FileWrapper, \
|
from appy.gen.utils import sequenceTypes, GroupDescr, Keywords, FileWrapper, \
|
||||||
getClassName, SomeObjects, AppyObject
|
getClassName, SomeObjects
|
||||||
import appy.pod
|
import appy.pod
|
||||||
from appy.pod.renderer import Renderer
|
from appy.pod.renderer import Renderer
|
||||||
from appy.shared.data import countries
|
from appy.shared.data import countries
|
||||||
|
@ -16,7 +17,7 @@ r, w, d = ('read', 'write', 'delete')
|
||||||
digit = re.compile('[0-9]')
|
digit = re.compile('[0-9]')
|
||||||
alpha = re.compile('[a-zA-Z0-9]')
|
alpha = re.compile('[a-zA-Z0-9]')
|
||||||
letter = re.compile('[a-zA-Z]')
|
letter = re.compile('[a-zA-Z]')
|
||||||
nullValues = (None, '', ' ')
|
nullValues = (None, '', [])
|
||||||
validatorTypes = (types.FunctionType, types.UnboundMethodType,
|
validatorTypes = (types.FunctionType, types.UnboundMethodType,
|
||||||
type(re.compile('')))
|
type(re.compile('')))
|
||||||
emptyTuple = ()
|
emptyTuple = ()
|
||||||
|
@ -729,7 +730,7 @@ class Type:
|
||||||
def getValue(self, obj):
|
def getValue(self, obj):
|
||||||
'''Gets, on_obj, the value conforming to self's type definition.'''
|
'''Gets, on_obj, the value conforming to self's type definition.'''
|
||||||
value = getattr(obj.aq_base, self.name, None)
|
value = getattr(obj.aq_base, self.name, None)
|
||||||
if (value == None):
|
if self.isEmptyValue(value):
|
||||||
# If there is no value, get the default value if any
|
# If there is no value, get the default value if any
|
||||||
if not self.editDefault:
|
if not self.editDefault:
|
||||||
# Return self.default, of self.default() if it is a method
|
# Return self.default, of self.default() if it is a method
|
||||||
|
@ -2243,10 +2244,6 @@ class List(Type):
|
||||||
for n, field in self.fields:
|
for n, field in self.fields:
|
||||||
if n == name: return field
|
if n == name: return field
|
||||||
|
|
||||||
def isEmptyValue(self, value, obj=None):
|
|
||||||
'''Returns True if the p_value must be considered as an empty value.'''
|
|
||||||
return not value
|
|
||||||
|
|
||||||
def getRequestValue(self, request):
|
def getRequestValue(self, request):
|
||||||
'''Concatenates the list from distinct form elements in the request.'''
|
'''Concatenates the list from distinct form elements in the request.'''
|
||||||
prefix = self.name + '*' + self.fields[0][0] + '*'
|
prefix = self.name + '*' + self.fields[0][0] + '*'
|
||||||
|
@ -2254,7 +2251,7 @@ class List(Type):
|
||||||
for key in request.keys():
|
for key in request.keys():
|
||||||
if not key.startswith(prefix): continue
|
if not key.startswith(prefix): continue
|
||||||
# I have found a row. Gets its index
|
# I have found a row. Gets its index
|
||||||
row = AppyObject()
|
row = Object()
|
||||||
rowIndex = int(key.split('*')[-1])
|
rowIndex = int(key.split('*')[-1])
|
||||||
if rowIndex == -1: continue # Ignore the template row.
|
if rowIndex == -1: continue # Ignore the template row.
|
||||||
for name, field in self.fields:
|
for name, field in self.fields:
|
||||||
|
@ -2271,10 +2268,6 @@ class List(Type):
|
||||||
keys = res.keys()
|
keys = res.keys()
|
||||||
keys.sort()
|
keys.sort()
|
||||||
res = [res[key] for key in keys]
|
res = [res[key] for key in keys]
|
||||||
print 'REQUEST VALUE FOR LIST (%d)' % len(res)
|
|
||||||
for value in res:
|
|
||||||
for k, v in value.__dict__.iteritems():
|
|
||||||
print k, '=', v
|
|
||||||
# I store in the request this computed value. This way, when individual
|
# I store in the request this computed value. This way, when individual
|
||||||
# subFields will need to get their value, they will take it from here,
|
# subFields will need to get their value, they will take it from here,
|
||||||
# instead of taking it from the specific request key. Indeed, specific
|
# instead of taking it from the specific request key. Indeed, specific
|
||||||
|
@ -2290,14 +2283,13 @@ class List(Type):
|
||||||
setattr(v, name, field.getStorableValue(getattr(v, name)))
|
setattr(v, name, field.getStorableValue(getattr(v, name)))
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def getInnerValue(self, obj, name, i):
|
def getInnerValue(self, outerValue, name, i):
|
||||||
'''Returns the value of inner field named p_name in row number p_i
|
'''Returns the value of inner field named p_name in row number p_i
|
||||||
with the list of values from this field on p_obj.'''
|
within the whole list of values p_outerValue.'''
|
||||||
if i == -1: return ''
|
if i == -1: return ''
|
||||||
value = getattr(obj, self.name, None)
|
if not outerValue: return ''
|
||||||
if not value: return ''
|
if i >= len(outerValue): return ''
|
||||||
if i >= len(value): return ''
|
return getattr(outerValue[i], name, '')
|
||||||
return getattr(value[i], name, '')
|
|
||||||
|
|
||||||
# Workflow-specific types and default workflows --------------------------------
|
# Workflow-specific types and default workflows --------------------------------
|
||||||
appyToZopePermissions = {
|
appyToZopePermissions = {
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
import os, os.path, sys, types, mimetypes, urllib, cgi
|
import os, os.path, sys, types, mimetypes, urllib, cgi
|
||||||
|
from appy import Object
|
||||||
import appy.gen
|
import appy.gen
|
||||||
from appy.gen import Type, String, Selection, Role, No, WorkflowAnonymous, \
|
from appy.gen import Type, String, Selection, Role, No, WorkflowAnonymous, \
|
||||||
Transition, Permission
|
Transition, Permission
|
||||||
|
@ -206,9 +207,9 @@ class BaseMixin:
|
||||||
return self.goto(urlBack)
|
return self.goto(urlBack)
|
||||||
|
|
||||||
# Object for storing validation errors
|
# Object for storing validation errors
|
||||||
errors = AppyObject()
|
errors = Object()
|
||||||
# Object for storing the (converted) values from the request
|
# Object for storing the (converted) values from the request
|
||||||
values = AppyObject()
|
values = Object()
|
||||||
|
|
||||||
# Trigger field-specific validation
|
# Trigger field-specific validation
|
||||||
self.intraFieldValidation(errors, values)
|
self.intraFieldValidation(errors, values)
|
||||||
|
@ -435,7 +436,8 @@ class BaseMixin:
|
||||||
# broken on returned object.
|
# broken on returned object.
|
||||||
return getattr(self, methodName, None)
|
return getattr(self, methodName, None)
|
||||||
|
|
||||||
def getFieldValue(self, name, onlyIfSync=False, layoutType=None):
|
def getFieldValue(self, name, onlyIfSync=False, layoutType=None,
|
||||||
|
outerValue=None):
|
||||||
'''Returns the database value of field named p_name for p_self.
|
'''Returns the database value of field named p_name for p_self.
|
||||||
If p_onlyIfSync is True, it returns the value only if appyType can be
|
If p_onlyIfSync is True, it returns the value only if appyType can be
|
||||||
retrieved in synchronous mode.'''
|
retrieved in synchronous mode.'''
|
||||||
|
@ -445,7 +447,8 @@ class BaseMixin:
|
||||||
if '*' not in name: return appyType.getValue(self)
|
if '*' not in name: return appyType.getValue(self)
|
||||||
# The field is an inner field from a List.
|
# The field is an inner field from a List.
|
||||||
listName, name, i = name.split('*')
|
listName, name, i = name.split('*')
|
||||||
return self.getAppyType(listName).getInnerValue(self, name, int(i))
|
listType = self.getAppyType(listName)
|
||||||
|
return listType.getInnerValue(outerValue, name, int(i))
|
||||||
|
|
||||||
def getFormattedFieldValue(self, name, value):
|
def getFormattedFieldValue(self, name, value):
|
||||||
'''Gets a nice, string representation of p_value which is a value from
|
'''Gets a nice, string representation of p_value which is a value from
|
||||||
|
|
|
@ -145,7 +145,7 @@
|
||||||
</tal:noObject>
|
</tal:noObject>
|
||||||
|
|
||||||
<tal:comment replace="nothing">If there is an object...</tal:comment>
|
<tal:comment replace="nothing">If there is an object...</tal:comment>
|
||||||
<tal:objectIsPresent condition="objs">
|
<tal:objectIsPresent condition="python: objs">
|
||||||
<tal:obj repeat="obj objs">
|
<tal:obj repeat="obj objs">
|
||||||
<td tal:define="includeShownInfo python:True"><metal:showObjectTitle use-macro="app/skyn/widgets/ref/macros/objectTitle" /></td>
|
<td tal:define="includeShownInfo python:True"><metal:showObjectTitle use-macro="app/skyn/widgets/ref/macros/objectTitle" /></td>
|
||||||
</tal:obj>
|
</tal:obj>
|
||||||
|
@ -161,7 +161,7 @@
|
||||||
(<span tal:replace="totalNumber"/>)
|
(<span tal:replace="totalNumber"/>)
|
||||||
<metal:plusIcon use-macro="app/skyn/widgets/ref/macros/plusIcon"/>
|
<metal:plusIcon use-macro="app/skyn/widgets/ref/macros/plusIcon"/>
|
||||||
<tal:comment replace="nothing">The search icon if field is queryable</tal:comment>
|
<tal:comment replace="nothing">The search icon if field is queryable</tal:comment>
|
||||||
<a tal:condition="appyType/queryable"
|
<a tal:condition="python: objs and appyType['queryable']"
|
||||||
tal:attributes="href python: '%s/skyn/search?type_name=%s&ref=%s:%s' % (tool.absolute_url(), linkedPortalType, contextObj.UID(), appyType['name'])">
|
tal:attributes="href python: '%s/skyn/search?type_name=%s&ref=%s:%s' % (tool.absolute_url(), linkedPortalType, contextObj.UID(), appyType['name'])">
|
||||||
<img src="search.gif" tal:attributes="title python: _('search_objects')"/></a>
|
<img src="search.gif" tal:attributes="title python: _('search_objects')"/></a>
|
||||||
</legend>
|
</legend>
|
||||||
|
|
|
@ -59,7 +59,8 @@
|
||||||
layout python: widget['layouts'][layoutType];
|
layout python: widget['layouts'][layoutType];
|
||||||
name widgetName| widget/name;
|
name widgetName| widget/name;
|
||||||
sync python: widget['sync'][layoutType];
|
sync python: widget['sync'][layoutType];
|
||||||
rawValue python: contextObj.getFieldValue(name, onlyIfSync=True, layoutType=layoutType);
|
outerValue value|nothing;
|
||||||
|
rawValue python: contextObj.getFieldValue(name,onlyIfSync=True,layoutType=layoutType,outerValue=outerValue);
|
||||||
value python: contextObj.getFormattedFieldValue(name, rawValue);
|
value python: contextObj.getFormattedFieldValue(name, rawValue);
|
||||||
requestValue python: contextObj.getRequestFieldValue(name);
|
requestValue python: contextObj.getRequestFieldValue(name);
|
||||||
inRequest python: request.has_key(name);
|
inRequest python: request.has_key(name);
|
||||||
|
|
|
@ -158,9 +158,6 @@ def produceNiceMessage(msg):
|
||||||
res += c
|
res += c
|
||||||
return res
|
return res
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
|
||||||
class AppyObject: pass
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
class SomeObjects:
|
class SomeObjects:
|
||||||
'''Represents a bunch of objects retrieved from a reference or a query in
|
'''Represents a bunch of objects retrieved from a reference or a query in
|
||||||
|
|
64
pod/parts.py
Normal file
64
pod/parts.py
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
class OdtTable:
|
||||||
|
'''This class allows to construct an ODT table programmatically.'''
|
||||||
|
# Some namespace definitions
|
||||||
|
tns = 'table:'
|
||||||
|
txns = 'text:'
|
||||||
|
|
||||||
|
def __init__(self, name, paraStyle, cellStyle, nbOfCols,
|
||||||
|
paraHeaderStyle=None, cellHeaderStyle=None):
|
||||||
|
# An ODT table must have a name.
|
||||||
|
self.name = name
|
||||||
|
# The default style of every paragraph within cells
|
||||||
|
self.paraStyle = paraStyle
|
||||||
|
# The default style of every cell
|
||||||
|
self.cellStyle = cellStyle
|
||||||
|
# The total number of columns
|
||||||
|
self.nbOfCols = nbOfCols
|
||||||
|
# The default style of every paragraph within a header cell
|
||||||
|
self.paraHeaderStyle = paraHeaderStyle or paraStyle
|
||||||
|
# The default style of every header cell
|
||||||
|
self.cellHeaderStyle = cellHeaderStyle or cellStyle
|
||||||
|
# The buffer where the resulting table will be rendered
|
||||||
|
self.res = ''
|
||||||
|
|
||||||
|
def dumpCell(self, content, span=1, header=False,
|
||||||
|
paraStyle=None, cellStyle=None):
|
||||||
|
'''Dumps a cell in the table. If no specific p_paraStyle (p_cellStyle)
|
||||||
|
is given, self.paraStyle (self.cellStyle) is used, excepted if
|
||||||
|
p_header is True: in that case, self.paraHeaderStyle
|
||||||
|
(self.cellHeaderStyle) is used.'''
|
||||||
|
if not paraStyle:
|
||||||
|
if header: paraStyle = self.paraHeaderStyle
|
||||||
|
else: paraStyle = self.paraStyle
|
||||||
|
if not cellStyle:
|
||||||
|
if header: cellStyle = self.cellHeaderStyle
|
||||||
|
else: cellStyle = self.cellStyle
|
||||||
|
self.res += '<%stable-cell %sstyle-name="%s" ' \
|
||||||
|
'%snumber-columns-spanned="%d">' % \
|
||||||
|
(self.tns, self.tns, cellStyle, self.tns, span)
|
||||||
|
self.res += '<%sp %sstyle-name="%s">%s</%sp>' % \
|
||||||
|
(self.txns, self.txns, paraStyle, content, self.txns)
|
||||||
|
self.res += '</%stable-cell>' % self.tns
|
||||||
|
|
||||||
|
def startRow(self):
|
||||||
|
self.res += '<%stable-row>' % self.tns
|
||||||
|
|
||||||
|
def endRow(self):
|
||||||
|
self.res += '</%stable-row>' % self.tns
|
||||||
|
|
||||||
|
def startTable(self):
|
||||||
|
self.res += '<%stable %sname="%s">' % (self.tns, self.tns, self.name)
|
||||||
|
self.res += '<%stable-column %snumber-columns-repeated="%d"/>' % \
|
||||||
|
(self.tns, self.tns, self.nbOfCols)
|
||||||
|
|
||||||
|
def endTable(self):
|
||||||
|
self.res += '</%stable>' % self.tns
|
||||||
|
|
||||||
|
def dumpFloat(self, number):
|
||||||
|
return str(round(number, 2))
|
||||||
|
|
||||||
|
def get(self):
|
||||||
|
'''Returns the whole table.'''
|
||||||
|
return self.res.decode('utf-8')
|
||||||
|
# ------------------------------------------------------------------------------
|
|
@ -44,7 +44,4 @@ class UnicodeBuffer:
|
||||||
self.buffer.append(unicode(s))
|
self.buffer.append(unicode(s))
|
||||||
def getValue(self):
|
def getValue(self):
|
||||||
return u''.join(self.buffer)
|
return u''.join(self.buffer)
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
|
||||||
class Dummy: pass
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
|
@ -212,26 +212,25 @@ class HtmlDiff:
|
||||||
ratio = difflib.SequenceMatcher(a=s1.lower(), b=s2.lower()).ratio()
|
ratio = difflib.SequenceMatcher(a=s1.lower(), b=s2.lower()).ratio()
|
||||||
return ratio > self.diffRatio
|
return ratio > self.diffRatio
|
||||||
|
|
||||||
def isEmpty(self, l):
|
def splitTagAndContent(self, line):
|
||||||
'''Is list p_l empty ?'''
|
'''p_line is a XHTML tag with content. This method returns a tuple
|
||||||
return not l or ( (len(l) == 1) and (l[0] in ('', '\r')))
|
(startTag, content), where p_startTag is the isolated start tag and
|
||||||
|
content is the tag content.'''
|
||||||
def getTagContent(self, line):
|
i = line.find('>')+1
|
||||||
'''p_lines is a XHTML tag with content. This method returns the content
|
return line[0:i], line[i:line.rfind('<')]
|
||||||
of the tag, removing start and end tags.'''
|
|
||||||
return line[line.find('>')+1:line.rfind('<')]
|
|
||||||
|
|
||||||
def getLineAndType(self, line):
|
def getLineAndType(self, line):
|
||||||
'''p_line is a string that can already have been surrounded by an
|
'''p_line is a string that can already have been surrounded by an
|
||||||
"insert" or "delete" tag. This is what we try to determine here.
|
"insert" or "delete" tag. This is what we try to determine here.
|
||||||
This method returns a tuple (type, line, innerDiffs), where "type"
|
This method returns a tuple (type, line, innerDiffs, outerTag),
|
||||||
can be:
|
where "type" can be:
|
||||||
* "insert" if it has already been flagged as inserted;
|
* "insert" if it has already been flagged as inserted;
|
||||||
* "delete" if it has already been flagged as deleted;
|
* "delete" if it has already been flagged as deleted;
|
||||||
* None else;
|
* None else;
|
||||||
"line" holds the original parameter p_line, excepted:
|
"line" holds the original parameter p_line, excepted:
|
||||||
* if type="insert". In that case, the surrounding insert tag has been
|
* if type="insert". In that case, the surrounding insert tag has been
|
||||||
removed;
|
removed and placed into "outerTag" (the outer start tag to be more
|
||||||
|
precise);
|
||||||
* if inner diff tags (insert or delete) are found. In that case,
|
* if inner diff tags (insert or delete) are found. In that case,
|
||||||
- if inner "insert" tags are found, they are removed but their
|
- if inner "insert" tags are found, they are removed but their
|
||||||
content is kept;
|
content is kept;
|
||||||
|
@ -240,10 +239,15 @@ class HtmlDiff:
|
||||||
- "innerDiffs" holds the list of re.MatchObjects instances
|
- "innerDiffs" holds the list of re.MatchObjects instances
|
||||||
representing the found inner tags.
|
representing the found inner tags.
|
||||||
'''
|
'''
|
||||||
if line.startswith(self.divDeletePrefix): return ('delete', line, None)
|
if line.startswith(self.divDeletePrefix):
|
||||||
|
return ('delete', line, None, None)
|
||||||
if line.startswith(self.divInsertPrefix):
|
if line.startswith(self.divInsertPrefix):
|
||||||
# Return the line without the surrounding tag.
|
# Return the line without the surrounding tag.
|
||||||
return ('insert', self.getTagContent(line), None)
|
action = 'insert'
|
||||||
|
outerTag, line = self.splitTagAndContent(line)
|
||||||
|
else:
|
||||||
|
action = None
|
||||||
|
outerTag = None
|
||||||
# Replace found inner inserts with their content.
|
# Replace found inner inserts with their content.
|
||||||
innerDiffs = []
|
innerDiffs = []
|
||||||
while True:
|
while True:
|
||||||
|
@ -256,7 +260,7 @@ class HtmlDiff:
|
||||||
if match.group(1) == 'insert':
|
if match.group(1) == 'insert':
|
||||||
content = match.group(2)
|
content = match.group(2)
|
||||||
line = line[:match.start()] + content + line[match.end():]
|
line = line[:match.start()] + content + line[match.end():]
|
||||||
return (None, line, innerDiffs)
|
return (action, line, innerDiffs, outerTag)
|
||||||
|
|
||||||
def getSeqDiff(self, seqA, seqB):
|
def getSeqDiff(self, seqA, seqB):
|
||||||
'''p_seqA and p_seqB are lists of strings. Here we will try to identify
|
'''p_seqA and p_seqB are lists of strings. Here we will try to identify
|
||||||
|
@ -278,7 +282,7 @@ class HtmlDiff:
|
||||||
# Scan every string from p_seqA and try to find a similar string in
|
# Scan every string from p_seqA and try to find a similar string in
|
||||||
# p_seqB.
|
# p_seqB.
|
||||||
while i < len(seqA):
|
while i < len(seqA):
|
||||||
pastAction, lineSeqA, innerDiffs = self.getLineAndType(seqA[i])
|
pastAction, lineA, innerDiffs, outerTag=self.getLineAndType(seqA[i])
|
||||||
if pastAction == 'delete':
|
if pastAction == 'delete':
|
||||||
# We will consider this line as "equal" because it already has
|
# We will consider this line as "equal" because it already has
|
||||||
# been noted as deleted in a previous diff.
|
# been noted as deleted in a previous diff.
|
||||||
|
@ -293,10 +297,10 @@ class HtmlDiff:
|
||||||
# be found in seqB.
|
# be found in seqB.
|
||||||
res.append( ('equal', seqA[i]) )
|
res.append( ('equal', seqA[i]) )
|
||||||
else:
|
else:
|
||||||
# Try to find a line in seqB which is similar to lineSeqA.
|
# Try to find a line in seqB which is similar to lineA.
|
||||||
similarFound = False
|
similarFound = False
|
||||||
for j in range(k, len(seqB)):
|
for j in range(k, len(seqB)):
|
||||||
if self.isSimilar(lineSeqA, seqB[j]):
|
if self.isSimilar(lineA, seqB[j]):
|
||||||
similarFound = True
|
similarFound = True
|
||||||
# Strings between indices k and j in p_seqB must be
|
# Strings between indices k and j in p_seqB must be
|
||||||
# considered as inserted, because no similar line exists
|
# considered as inserted, because no similar line exists
|
||||||
|
@ -304,16 +308,15 @@ class HtmlDiff:
|
||||||
if k < j:
|
if k < j:
|
||||||
for line in seqB[k:j]: res.append(('insert', line))
|
for line in seqB[k:j]: res.append(('insert', line))
|
||||||
# Similar strings are appended in a 'replace' entry,
|
# Similar strings are appended in a 'replace' entry,
|
||||||
# excepted if lineSeqA is already an insert from a
|
# excepted if lineA is already an insert from a
|
||||||
# previous diff: in this case, we keep the "old"
|
# previous diff: in this case, we keep the "old"
|
||||||
# version: the new one is the same, but for which we
|
# version: the new one is the same, but for which we
|
||||||
# don't remember who updated it.
|
# don't remember who updated it.
|
||||||
if (pastAction == 'insert') and (lineSeqA == seqB[j]):
|
if (pastAction == 'insert') and (lineA == seqB[j]):
|
||||||
res.append( ('equal', seqA[i]) )
|
res.append( ('equal', seqA[i]) )
|
||||||
# TODO: manage lineSeqA != seqB[j]
|
|
||||||
else:
|
else:
|
||||||
res.append(('replace', (lineSeqA, seqB[j],
|
res.append(('replace', (lineA, seqB[j],
|
||||||
innerDiffs)))
|
innerDiffs, outerTag)))
|
||||||
k = j+1
|
k = j+1
|
||||||
break
|
break
|
||||||
if not similarFound: res.append( ('delete', seqA[i]) )
|
if not similarFound: res.append( ('delete', seqA[i]) )
|
||||||
|
@ -337,6 +340,16 @@ class HtmlDiff:
|
||||||
if trailSpace: res[-1] = res[-1] + sep
|
if trailSpace: res[-1] = res[-1] + sep
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
garbage = ('', '\r')
|
||||||
|
def removeGarbage(self, l):
|
||||||
|
'''Removes from list p_l elements that have no interest, like blank
|
||||||
|
strings or considered as is.'''
|
||||||
|
i = len(l)-1
|
||||||
|
while i >= 0:
|
||||||
|
if l[i] in self.garbage: del l[i]
|
||||||
|
i -= 1
|
||||||
|
return l
|
||||||
|
|
||||||
def getHtmlDiff(self, old, new, sep):
|
def getHtmlDiff(self, old, new, sep):
|
||||||
'''Returns the differences between p_old and p_new. Result is a string
|
'''Returns the differences between p_old and p_new. Result is a string
|
||||||
containing the comparison in HTML format. p_sep is used for turning
|
containing the comparison in HTML format. p_sep is used for turning
|
||||||
|
@ -352,29 +365,27 @@ class HtmlDiff:
|
||||||
matcher = difflib.SequenceMatcher()
|
matcher = difflib.SequenceMatcher()
|
||||||
matcher.set_seqs(a,b)
|
matcher.set_seqs(a,b)
|
||||||
for action, i1, i2, j1, j2 in matcher.get_opcodes():
|
for action, i1, i2, j1, j2 in matcher.get_opcodes():
|
||||||
chunkA = a[i1:i2]
|
chunkA = self.removeGarbage(a[i1:i2])
|
||||||
chunkB = b[j1:j2]
|
chunkB = self.removeGarbage(b[j1:j2])
|
||||||
aIsEmpty = self.isEmpty(chunkA)
|
|
||||||
bIsEmpty = self.isEmpty(chunkB)
|
|
||||||
toAdd = None
|
toAdd = None
|
||||||
if action == 'equal':
|
if action == 'equal':
|
||||||
if not aIsEmpty: toAdd = sep.join(chunkA)
|
if chunkA: toAdd = sep.join(chunkA)
|
||||||
elif action == 'insert':
|
elif action == 'insert':
|
||||||
if not bIsEmpty:
|
if chunkB:
|
||||||
toAdd = self.getModifiedChunk(chunkB, action, sep)
|
toAdd = self.getModifiedChunk(chunkB, action, sep)
|
||||||
elif action == 'delete':
|
elif action == 'delete':
|
||||||
if not aIsEmpty:
|
if chunkA:
|
||||||
toAdd = self.getModifiedChunk(chunkA, action, sep)
|
toAdd = self.getModifiedChunk(chunkA, action, sep)
|
||||||
elif action == 'replace':
|
elif action == 'replace':
|
||||||
if aIsEmpty and bIsEmpty:
|
if not chunkA and not chunkB:
|
||||||
toAdd = ''
|
toAdd = ''
|
||||||
elif aIsEmpty:
|
elif not chunkA:
|
||||||
# Was an addition, not a replacement
|
# Was an addition, not a replacement
|
||||||
toAdd = self.getModifiedChunk(chunkB, 'insert', sep)
|
toAdd = self.getModifiedChunk(chunkB, 'insert', sep)
|
||||||
elif bIsEmpty:
|
elif not chunkB:
|
||||||
# Was a deletion, not a replacement
|
# Was a deletion, not a replacement
|
||||||
toAdd = self.getModifiedChunk(chunkA, 'delete', sep)
|
toAdd = self.getModifiedChunk(chunkA, 'delete', sep)
|
||||||
else: # At least, a true replacement (grr difflib)
|
else: # At least, a true replacement
|
||||||
if sep == '\n':
|
if sep == '\n':
|
||||||
# We know that some lines have been replaced from a to
|
# We know that some lines have been replaced from a to
|
||||||
# b. By identifying similarities between those lines,
|
# b. By identifying similarities between those lines,
|
||||||
|
@ -387,7 +398,7 @@ class HtmlDiff:
|
||||||
elif sAction == 'equal':
|
elif sAction == 'equal':
|
||||||
toAdd += line
|
toAdd += line
|
||||||
elif sAction == 'replace':
|
elif sAction == 'replace':
|
||||||
lineA, lineB, previousDiffsA = line
|
lineA, lineB, previousDiffsA, outerTag = line
|
||||||
# Investigate further here and explore
|
# Investigate further here and explore
|
||||||
# differences at the *word* level between lineA
|
# differences at the *word* level between lineA
|
||||||
# and lineB. As a preamble, and in order to
|
# and lineB. As a preamble, and in order to
|
||||||
|
@ -405,6 +416,10 @@ class HtmlDiff:
|
||||||
if previousDiffsA:
|
if previousDiffsA:
|
||||||
merger= Merger(lineA, toAdd, previousDiffsA)
|
merger= Merger(lineA, toAdd, previousDiffsA)
|
||||||
toAdd = merger.merge()
|
toAdd = merger.merge()
|
||||||
|
# Rewrap line into outerTag if lineA was a line
|
||||||
|
# tagged as previously inserted.
|
||||||
|
if outerTag:
|
||||||
|
toAdd = outerTag + toAdd + '</div>'
|
||||||
else:
|
else:
|
||||||
toAdd = self.getModifiedChunk(chunkA, 'delete', sep)
|
toAdd = self.getModifiedChunk(chunkA, 'delete', sep)
|
||||||
toAdd += self.getModifiedChunk(chunkB, 'insert', sep)
|
toAdd += self.getModifiedChunk(chunkB, 'insert', sep)
|
||||||
|
|
|
@ -448,7 +448,8 @@ class XmlMarshaller:
|
||||||
self.rootElementName = rootTag
|
self.rootElementName = rootTag
|
||||||
# The namespaces that will be defined at the root of the XML message.
|
# The namespaces that will be defined at the root of the XML message.
|
||||||
# It is a dict whose keys are namespace prefixes and whose values are
|
# It is a dict whose keys are namespace prefixes and whose values are
|
||||||
# namespace URLs.
|
# namespace URLs. If you want to specify a default namespace, specify an
|
||||||
|
# entry with an empty string as a key.
|
||||||
self.namespaces = namespaces
|
self.namespaces = namespaces
|
||||||
# The following dict will tell which XML tags will get which namespace
|
# The following dict will tell which XML tags will get which namespace
|
||||||
# prefix ({s_tagName: s_prefix}). Special optional dict entry
|
# prefix ({s_tagName: s_prefix}). Special optional dict entry
|
||||||
|
@ -475,8 +476,12 @@ class XmlMarshaller:
|
||||||
res.write('<'); res.write(tagName)
|
res.write('<'); res.write(tagName)
|
||||||
# Dumps namespace definitions if any
|
# Dumps namespace definitions if any
|
||||||
for prefix, url in self.namespaces.iteritems():
|
for prefix, url in self.namespaces.iteritems():
|
||||||
res.write(' xmlns:%s="%s"' % (prefix, url))
|
if not prefix:
|
||||||
# Dumps Appy- or Plone-specific attributed
|
pre = 'xmlns' # The default namespace
|
||||||
|
else:
|
||||||
|
pre = 'xmlns:%s' % prefix
|
||||||
|
res.write(' %s="%s"' % (pre, url))
|
||||||
|
# Dumps Appy- or Plone-specific attribute
|
||||||
if self.objectType != 'popo':
|
if self.objectType != 'popo':
|
||||||
res.write(' type="object" id="%s"' % instance.UID())
|
res.write(' type="object" id="%s"' % instance.UID())
|
||||||
res.write('>')
|
res.write('>')
|
||||||
|
@ -572,6 +577,11 @@ class XmlMarshaller:
|
||||||
|
|
||||||
def dumpField(self, res, fieldName, fieldValue, fieldType='basic'):
|
def dumpField(self, res, fieldName, fieldValue, fieldType='basic'):
|
||||||
'''Dumps in p_res, the value of the p_field for p_instance.'''
|
'''Dumps in p_res, the value of the p_field for p_instance.'''
|
||||||
|
# As a preamble, manage special case of p_fieldName == "_any". In that
|
||||||
|
# case, p_fieldValue corresponds to a previously marshalled string that
|
||||||
|
# must be included as is here, without dumping the tag name.
|
||||||
|
if fieldName == '_any': self.dumpValue(res, fieldValue, None)
|
||||||
|
# Now, dump "normal" fields.
|
||||||
fieldTag = self.getTagName(fieldName)
|
fieldTag = self.getTagName(fieldName)
|
||||||
res.write('<'); res.write(fieldTag)
|
res.write('<'); res.write(fieldTag)
|
||||||
# Dump the type of the field as an XML attribute
|
# Dump the type of the field as an XML attribute
|
||||||
|
|
Loading…
Reference in a new issue