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 -*-
|
||||
# ------------------------------------------------------------------------------
|
||||
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 = {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
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))
|
||||
def getValue(self):
|
||||
return u''.join(self.buffer)
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class Dummy: pass
|
||||
# ------------------------------------------------------------------------------
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue