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:
Gaetan Delannay 2011-10-26 10:21:09 +02:00
parent 1ebcbb7b34
commit 3ab6cec7d6
9 changed files with 147 additions and 68 deletions

View file

@ -1,11 +1,12 @@
# -*- coding: utf-8 -*-
# ------------------------------------------------------------------------------
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 defaultFieldLayouts
from appy.gen.po import PoMessage
from appy.gen.utils import sequenceTypes, GroupDescr, Keywords, FileWrapper, \
getClassName, SomeObjects, AppyObject
getClassName, SomeObjects
import appy.pod
from appy.pod.renderer import Renderer
from appy.shared.data import countries
@ -16,7 +17,7 @@ r, w, d = ('read', 'write', 'delete')
digit = re.compile('[0-9]')
alpha = re.compile('[a-zA-Z0-9]')
letter = re.compile('[a-zA-Z]')
nullValues = (None, '', ' ')
nullValues = (None, '', [])
validatorTypes = (types.FunctionType, types.UnboundMethodType,
type(re.compile('')))
emptyTuple = ()
@ -729,7 +730,7 @@ class Type:
def getValue(self, obj):
'''Gets, on_obj, the value conforming to self's type definition.'''
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 not self.editDefault:
# Return self.default, of self.default() if it is a method
@ -2243,10 +2244,6 @@ class List(Type):
for n, field in self.fields:
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):
'''Concatenates the list from distinct form elements in the request.'''
prefix = self.name + '*' + self.fields[0][0] + '*'
@ -2254,7 +2251,7 @@ class List(Type):
for key in request.keys():
if not key.startswith(prefix): continue
# I have found a row. Gets its index
row = AppyObject()
row = Object()
rowIndex = int(key.split('*')[-1])
if rowIndex == -1: continue # Ignore the template row.
for name, field in self.fields:
@ -2271,10 +2268,6 @@ class List(Type):
keys = res.keys()
keys.sort()
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
# subFields will need to get their value, they will take it from here,
# 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)))
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
with the list of values from this field on p_obj.'''
within the whole list of values p_outerValue.'''
if i == -1: return ''
value = getattr(obj, self.name, None)
if not value: return ''
if i >= len(value): return ''
return getattr(value[i], name, '')
if not outerValue: return ''
if i >= len(outerValue): return ''
return getattr(outerValue[i], name, '')
# Workflow-specific types and default workflows --------------------------------
appyToZopePermissions = {

View file

@ -4,6 +4,7 @@
# ------------------------------------------------------------------------------
import os, os.path, sys, types, mimetypes, urllib, cgi
from appy import Object
import appy.gen
from appy.gen import Type, String, Selection, Role, No, WorkflowAnonymous, \
Transition, Permission
@ -206,9 +207,9 @@ class BaseMixin:
return self.goto(urlBack)
# Object for storing validation errors
errors = AppyObject()
errors = Object()
# Object for storing the (converted) values from the request
values = AppyObject()
values = Object()
# Trigger field-specific validation
self.intraFieldValidation(errors, values)
@ -435,7 +436,8 @@ class BaseMixin:
# broken on returned object.
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.
If p_onlyIfSync is True, it returns the value only if appyType can be
retrieved in synchronous mode.'''
@ -445,7 +447,8 @@ class BaseMixin:
if '*' not in name: return appyType.getValue(self)
# The field is an inner field from a List.
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):
'''Gets a nice, string representation of p_value which is a value from

View file

@ -145,7 +145,7 @@
</tal:noObject>
<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">
<td tal:define="includeShownInfo python:True"><metal:showObjectTitle use-macro="app/skyn/widgets/ref/macros/objectTitle" /></td>
</tal:obj>
@ -161,7 +161,7 @@
(<span tal:replace="totalNumber"/>)
<metal:plusIcon use-macro="app/skyn/widgets/ref/macros/plusIcon"/>
<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'])">
<img src="search.gif" tal:attributes="title python: _('search_objects')"/></a>
</legend>

View file

@ -59,7 +59,8 @@
layout python: widget['layouts'][layoutType];
name widgetName| widget/name;
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);
requestValue python: contextObj.getRequestFieldValue(name);
inRequest python: request.has_key(name);

View file

@ -158,9 +158,6 @@ def produceNiceMessage(msg):
res += c
return res
# ------------------------------------------------------------------------------
class AppyObject: pass
# ------------------------------------------------------------------------------
class SomeObjects:
'''Represents a bunch of objects retrieved from a reference or a query in

64
pod/parts.py Normal file
View 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')
# ------------------------------------------------------------------------------

View file

@ -44,7 +44,4 @@ class UnicodeBuffer:
self.buffer.append(unicode(s))
def getValue(self):
return u''.join(self.buffer)
# ------------------------------------------------------------------------------
class Dummy: pass
# ------------------------------------------------------------------------------

View file

@ -212,26 +212,25 @@ class HtmlDiff:
ratio = difflib.SequenceMatcher(a=s1.lower(), b=s2.lower()).ratio()
return ratio > self.diffRatio
def isEmpty(self, l):
'''Is list p_l empty ?'''
return not l or ( (len(l) == 1) and (l[0] in ('', '\r')))
def getTagContent(self, line):
'''p_lines is a XHTML tag with content. This method returns the content
of the tag, removing start and end tags.'''
return line[line.find('>')+1:line.rfind('<')]
def splitTagAndContent(self, line):
'''p_line is a XHTML tag with content. This method returns a tuple
(startTag, content), where p_startTag is the isolated start tag and
content is the tag content.'''
i = line.find('>')+1
return line[0:i], line[i:line.rfind('<')]
def getLineAndType(self, line):
'''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.
This method returns a tuple (type, line, innerDiffs), where "type"
can be:
This method returns a tuple (type, line, innerDiffs, outerTag),
where "type" can be:
* "insert" if it has already been flagged as inserted;
* "delete" if it has already been flagged as deleted;
* None else;
"line" holds the original parameter p_line, excepted:
* 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 "insert" tags are found, they are removed but their
content is kept;
@ -240,10 +239,15 @@ class HtmlDiff:
- "innerDiffs" holds the list of re.MatchObjects instances
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):
# 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.
innerDiffs = []
while True:
@ -256,7 +260,7 @@ class HtmlDiff:
if match.group(1) == 'insert':
content = match.group(2)
line = line[:match.start()] + content + line[match.end():]
return (None, line, innerDiffs)
return (action, line, innerDiffs, outerTag)
def getSeqDiff(self, seqA, seqB):
'''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
# p_seqB.
while i < len(seqA):
pastAction, lineSeqA, innerDiffs = self.getLineAndType(seqA[i])
pastAction, lineA, innerDiffs, outerTag=self.getLineAndType(seqA[i])
if pastAction == 'delete':
# We will consider this line as "equal" because it already has
# been noted as deleted in a previous diff.
@ -293,10 +297,10 @@ class HtmlDiff:
# be found in seqB.
res.append( ('equal', seqA[i]) )
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
for j in range(k, len(seqB)):
if self.isSimilar(lineSeqA, seqB[j]):
if self.isSimilar(lineA, seqB[j]):
similarFound = True
# Strings between indices k and j in p_seqB must be
# considered as inserted, because no similar line exists
@ -304,16 +308,15 @@ class HtmlDiff:
if k < j:
for line in seqB[k:j]: res.append(('insert', line))
# 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"
# version: the new one is the same, but for which we
# 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]) )
# TODO: manage lineSeqA != seqB[j]
else:
res.append(('replace', (lineSeqA, seqB[j],
innerDiffs)))
res.append(('replace', (lineA, seqB[j],
innerDiffs, outerTag)))
k = j+1
break
if not similarFound: res.append( ('delete', seqA[i]) )
@ -337,6 +340,16 @@ class HtmlDiff:
if trailSpace: res[-1] = res[-1] + sep
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):
'''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
@ -352,29 +365,27 @@ class HtmlDiff:
matcher = difflib.SequenceMatcher()
matcher.set_seqs(a,b)
for action, i1, i2, j1, j2 in matcher.get_opcodes():
chunkA = a[i1:i2]
chunkB = b[j1:j2]
aIsEmpty = self.isEmpty(chunkA)
bIsEmpty = self.isEmpty(chunkB)
chunkA = self.removeGarbage(a[i1:i2])
chunkB = self.removeGarbage(b[j1:j2])
toAdd = None
if action == 'equal':
if not aIsEmpty: toAdd = sep.join(chunkA)
if chunkA: toAdd = sep.join(chunkA)
elif action == 'insert':
if not bIsEmpty:
if chunkB:
toAdd = self.getModifiedChunk(chunkB, action, sep)
elif action == 'delete':
if not aIsEmpty:
if chunkA:
toAdd = self.getModifiedChunk(chunkA, action, sep)
elif action == 'replace':
if aIsEmpty and bIsEmpty:
if not chunkA and not chunkB:
toAdd = ''
elif aIsEmpty:
elif not chunkA:
# Was an addition, not a replacement
toAdd = self.getModifiedChunk(chunkB, 'insert', sep)
elif bIsEmpty:
elif not chunkB:
# Was a deletion, not a replacement
toAdd = self.getModifiedChunk(chunkA, 'delete', sep)
else: # At least, a true replacement (grr difflib)
else: # At least, a true replacement
if sep == '\n':
# We know that some lines have been replaced from a to
# b. By identifying similarities between those lines,
@ -387,7 +398,7 @@ class HtmlDiff:
elif sAction == 'equal':
toAdd += line
elif sAction == 'replace':
lineA, lineB, previousDiffsA = line
lineA, lineB, previousDiffsA, outerTag = line
# Investigate further here and explore
# differences at the *word* level between lineA
# and lineB. As a preamble, and in order to
@ -405,6 +416,10 @@ class HtmlDiff:
if previousDiffsA:
merger= Merger(lineA, toAdd, previousDiffsA)
toAdd = merger.merge()
# Rewrap line into outerTag if lineA was a line
# tagged as previously inserted.
if outerTag:
toAdd = outerTag + toAdd + '</div>'
else:
toAdd = self.getModifiedChunk(chunkA, 'delete', sep)
toAdd += self.getModifiedChunk(chunkB, 'insert', sep)

View file

@ -448,7 +448,8 @@ class XmlMarshaller:
self.rootElementName = rootTag
# 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
# 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
# The following dict will tell which XML tags will get which namespace
# prefix ({s_tagName: s_prefix}). Special optional dict entry
@ -475,8 +476,12 @@ class XmlMarshaller:
res.write('<'); res.write(tagName)
# Dumps namespace definitions if any
for prefix, url in self.namespaces.iteritems():
res.write(' xmlns:%s="%s"' % (prefix, url))
# Dumps Appy- or Plone-specific attributed
if not prefix:
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':
res.write(' type="object" id="%s"' % instance.UID())
res.write('>')
@ -572,6 +577,11 @@ class XmlMarshaller:
def dumpField(self, res, fieldName, fieldValue, fieldType='basic'):
'''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)
res.write('<'); res.write(fieldTag)
# Dump the type of the field as an XML attribute